Dev tutorials

9 mins read

Protect Spring Boot API with Multiple OAuth Servers

Learn how to configure a Spring Boot application that exposes API resources to protect API access using Bearer tokens that has been issued by multiple instance of Cloudentity authorization server as the trusted OAuth/OIDC provider.

Overview

Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can just run. In this tutorial, we will create and configure a Spring Boot based API application that uses Spring Security configuration to enforce API endpoint protection by accepting OAuth Bearer tokens only from a trusted OAuth/OIDC authorization server. This application expects the API callers to present a Bearer Token as defined in RFC6750 to access API resources. Any party in possession of a bearer token (a “bearer”) can use it to get access to the associated resources.

Cloudentity authorization platform have mulitple levels of governance policy checks to ensure OAuth access tokens are issued only after all policies are satisfied for the user and application access. The Cloudentity platform authorization server level multitenancy inherently allows you to model multiple authorization servers. In this tutorial, we will create multiple authorization servers within a Cloudentity tenant. The OAuth authorization server (part of the workspace) can issue OAuth access tokens with associated scopes.

Any API access within this application using the Bearer token issued by Cloudentity is protected by verifying that Cloudentity is the actual issuer of the presented Bearer OAuth access token using Spring Security configurations.

Spring Boot multiple OAuth server

Some of the main configurations showcased within this sample application are:

  • Validate and accept only access tokens issued by the configured trusted OAuth Authorization servers
  • Deny protected API resource access when accessed with:
    • No access token
    • Invalid access token
    • Access token from a different OAuth authorization server other than the ones in trusted list
    • iss claim does not match trusted authorization server issuer URL
    • Insufficient/missing scopes in the access tokens
  • Programatic scope check for API resources

Tip - Insight

Another approach to ensure proper authorization is to offload the authorization check to an upstream component like API Gateway and ensure the service cannot be accessed from any other component other the API Gateway trust domain. This integration model is similar to a sidecar implemenation in service meshes. This way the entire token validation can be offloaded. Cloudentity also provides an additional authorizer component that can be plug into modern API gateway ecosystems to validate, audit, and enforce access policy for API resource access by offloading that individual responsibility from the services themselves.

Reference repo

Check out the below github repo for complete source code of the reference application in this tutorial

Spring Boot Integration Samples

Prerequisites

  • Cloudentity SaaS tenant
  • Development
    • JDK 1.8+
    • Maven/Gradle
    • IDE of your choice

Configurations

In OAuth terminology, the Spring Boot service plays the role of the Resource Server with its own API endpoints. Gor consumption of these services, the resource server needs to define scopes that logically determine access to various APIs. Let’s go ahead and register the service as a Resource Server application within Cloudentity OAuth authorization server that depicts an OAuth resource server. During service registration, you can see that you have to define the scopes and attach them to the service. Notice that you can attach OAuth scope governance policies to each scope that you define.

Since we are demonstrating trusting multiple OAuth providers, we will use the built in workspace multitenancy feature within the Cloudentity platform that allows creation of independent OAuth authorization servers. Create multiple workspaces within Cloudentity and register the service and same associated scopes within each of the workspace. Each workspace is independent and represents an individual OAuth authorization server, hence the need to register the service twice in each workspace to showcase usage of multiple OAuth providers.

Scope Governance Policies and Their Benefits

One of the main highlights of Cloudentity authorization server are the scope governance policies. You can protect individual scopes with their own policies that can applied at either a client registration level or actual request flow to evaluate if the requesting user/application can actually get the scope back in access tokens.

Multitenancy

Cloudentity offers multitenancy for authorization servers within a single tenant. This gives the capability to create multiple authorization servers for various usecases.

Create Application

In this tutorial, we do not focus on the details of a Spring Boot application. You can use the above cloned repo to follow along with this tutorial or use this guide to create a vanilla Spring Boot application from ground up and, then, continue with the below configurations.

Define dependencies

It’s very important that proper Spring component dependencies are configured.

Spring OAuth Capabilities

Spring has recently moved most of the Spring Oauth capabilities into Spring Security. In case you are following some of the old tutorials in the internet you might run into outdated libs or references that still uses the old style of integration.

Choose your style for dependency management with either Maven/Gradle

  • Using Maven
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.7</version>
  </parent>

  <properties>
    <java.version>1.8</java.version>
    <spring.security.version>5.6.3</spring.security.version>
    <spring.boot.autoconfigure.version>2.6.7</spring.boot.autoconfigure.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>${spring.boot.autoconfigure.version}</version>
    </dependency>

    <!-- Spring security dependencies -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-oauth2-resource-server</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-oauth2-jose</artifactId>
      <version>${spring.security.version}</version>
    </dependency>

  </dependencies>

  • Using Gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:2.6.7'
    implementation 'org.springframework.security:spring-security-oauth2-resource-server:5.6.3'
    implementation 'org.springframework.security:spring-security-core:5.6.3'
    implementation 'org.springframework.security:spring-security-web:5.6.3'
    implementation 'org.springframework.security:spring-security-config:5.6.3'
    implementation 'org.springframework.security:spring-security-oauth2-jose:5.6.3'
}

Configure Multiple Trusted Authorization Providers

Let’s configure the multiple trusted authorization providers. It’s done by providing the trusted OAuth providers to the JwtIssuerAuthenticationManagerResolver instance and then providing that to the HTTP filter configuration for oauth2ResourceServer

Let’s see how can configure the 2 different authorization servers that we created within Cloudentity tenant into the Spring configuration

 Issuer 1: "https://{tid}.authz.cloudentity.io/{tid}/{aid1}"
 Issuer 2: "https://{tid}.authz.cloudentity.io/{tid}/{aid2}"

The {tid} variable stands for your tenant identifier. The {aid1} and {aid2} variables stands for the identifiers of your authorization servers (workspaces).

Tip

You can find the issuer uri within the Auth Settings » OAuth section in Cloudentity platform under AUTHORIZATION SERVER URL label

Issuer URL in Cloudentity

import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.stereotype.Component;

@Component
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ResourceServerAuthorizationConfig extends WebSecurityConfigurerAdapter {

  public void configure(HttpSecurity http) throws Exception {

    JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
        ("https://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations",
            "https://rtest.authz.cloudentity.io/rtest/ce-samples-oidc-client-apps");

    http.csrf().disable()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
        .antMatchers("/actuator/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
    }
  }
}

iss within the presented access token is first matched to see if it matches one the urls defined within JwtIssuerAuthenticationManagerResolver in the above configuration class. If it does not match, Spring Security rejects that access token. If it matches, then it uses the iss to construct the .well_known endpoint of the OAuth provider to fetch the jwks_uri. The jwks_uri is used to get the key used to verify the access token signature of the trusted authorization server. Thus the integrity of the access token presented to resource server is verified.

Sample Bearer access token:

{
    "scp": [
        "address",
        "email",
        "introspect_tokens",
        "openid",
        "phone",
        "profile",
        "revoke_tokens"
    ],
    "st": "public",
    "sub": "c6rnpqgh5kra1jev5o0g",
    "amr": [],
    "iss": "https://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations",
    "tid": "rtest",
    "aud": [
        "c6rnpqgh5kra1jev5o0g",
        "spiffe://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations/c6f9qqurvhrgrkeifa2g",
        "spiffe://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations/c6f9qqurvhrgrkeifa7g"
    ],
    "nbf": "2022-04-28T04:24:18Z",
    "idp": "",
    "exp": "2022-04-28T05:24:18Z",
    "aid": "ce-dev-playground-integrations",
    "iat": "2022-04-28T04:24:18Z",
    "jti": "60a329f2-d59c-47ed-be11-28e2e1f736f7"
}

Configure API Resource access protection

Now that we have configured the trusted OAuth provider within the Spring configuration, let’s configure Spring web security to enforce API access traffic based on API paths. In the below code snippet, we are going to enforce protection for all paths except the resources on /actuator/** and OPTIONS HTTP method on any path. This means any access to the rest of the resources served by this application needs to have a valid OAuth Bearer access token issued by the Cloudentity authorization server.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.stereotype.Component;

@Component
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ResourceServerAuthorizationConfig extends WebSecurityConfigurerAdapter {

  public void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
        .antMatchers("/actuator/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .oauth2ResourceServer()
        .jwt();
  }
}

Configure Scope Check

Let’s configure the scope check required for resource access. Scope check can be enforced using the Preauthorize annotation with the hasAuthority method and the scope of the resource. Sample usage of the annotation is @PreAuthorize("hasAuthority('SCOPE_openid')") which indicates that the openid scope is required in the Bearer token presented to access the specific API resource on which this annotation is applied.

import java.util.HashMap;
import java.util.Map;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api")
public class SampleController {

  @GetMapping("/jwt/info")
  public Map<String, Object> jwtInfoSample(){
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    Jwt j = (Jwt)auth.getCredentials();
    j.getClaims();
    return j.getClaims();
  }

  @PreAuthorize("hasAuthority('SCOPE_openid')")
  @RequestMapping("/sample/protected/openidscope")
  public Map<String, String> sampleScopeProtected() {
    Map<String, String> m = new HashMap<>();
    m.put("hasScope", "true");
    return m;
  }

  @PreAuthorize("hasAuthority('SCOPE_nonexistent')")
  @RequestMapping("/sample/protected/nonexistentscope")
  public Map<String, String> sampleNonExistentScope() {
    Map<String, String> m = new HashMap<>();
    m.put("hasScope", "true");
    return m;
  }

}

What Gets Verified

Using minimal Spring Boot configuration, indicating the authorization server’s issuer uri, this application configuration defaults to verifying the following claims:

  • iss
  • exp
  • nbf

Spring Security allows to further customize the validation checks for more attributes and for such scenarios, there are couple of advanced configuration options as described in this article.

Build and Test

Build Application

  • Build using Maven and Run
make build-run-maven
  • Build using Gradle and Run
make build-run-gradle

Register OAuth Client application(s) in Cloudentity

To test this application, we need to register OAuth client within multiple Cloudentity authorization servers and also subscribe to scopes defined by the Resource server. Tegister an OAuth client application within Cloudentity platform, so that we can fetch an access token from the Cloudentity authorization server using the registed OAuth client.

Tip

Make sure you are subscribing to scopes required by this application during the application registration

Tip

Since we want to test tokens from multiple authorization providers, Make sure to register 2 separate clients in different authorization servers within Cloudentity. Cloudentity is multitenant and you can create as many authorization servers (workspaces) as you like.

Request Access Token Using OAuth Client Application from Cloudentity

For sake of simplicity, we will use the above registered client and OAuth client_credentials grant flow to get the accessToken. The method of obtaining access token is irrelevant to this example and is used to demostrate only a specific test scenario.

curl --request POST \
  --url 'https://YOUR_CLOUDENTITY_ISSUER_URI/oauth/token' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=client_credentials \
  --data client_id=YOUR_CLIENT_ID \
  --data client_secret=YOUR_CLIENT_SECRET

Tip

Since we want to test tokens from multiple authorization providers, make sure you get tokens from corresponding Cloudentity workspaces (aka authorization servers) for various test scenarios.

Test API endpoints

  • Test endpoint wihout access token
curl -v -X GET http://localhost:8080/api/jwt/info

Response:

HTTP/1.1 401
  • Test endpoint with access token from a non trusted OAuth provider
curl -X GET \
  http://localhost:8080/api/jwt/info \
  -H 'Authorization: Bearer <PUT_YOUR_NON_TRUSTED_PROVIDER_ACCESS_TOKEN>'

Response:

HTTP/1.1 401
  • Valid access token and get JWT information

Provide access token from either of the trusted providers.

curl -X GET \
  http://localhost:8080/api/jwt/info \
  -H 'Authorization: Bearer <PUT_YOUR_ACCESS_TOKEN>'

Response:

{
    "scp": [
        "address",
        "email",
        "introspect_tokens",
        "openid",
        "phone",
        "profile",
        "revoke_tokens"
    ],
    "st": "public",
    "sub": "c6rnpqgh5kra1jev5o0g",
    "amr": [],
    "iss": "https://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations",
    "tid": "rtest",
    "aud": [
        "c6rnpqgh5kra1jev5o0g",
        "spiffe://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations/c6f9qqurvhrgrkeifa2g",
        "spiffe://rtest.authz.cloudentity.io/rtest/ce-dev-playground-integrations/c6f9qqurvhrgrkeifa7g"
    ],
    "nbf": "2022-04-28T04:24:18Z",
    "idp": "",
    "exp": "2022-04-28T05:24:18Z",
    "aid": "ce-dev-playground-integrations",
    "iat": "2022-04-28T04:24:18Z",
    "jti": "60a329f2-d59c-47ed-be11-28e2e1f736f7"
}
  • Valid access token with an existing scope

Provide access token from either of the trusted providers.

curl -X GET \
  http://localhost:8080/api/sample/protected/openidscope \
  -H 'Authorization: Bearer <PUT_YOUR_ACCESS_TOKEN>'

Response:

{
    "hasScope": "true"
}
  • Valid access token with a non existing scope

Provide access token from either of the trusted providers.

curl -v -X GET \
  http://localhost:8080/api/sample/protected/nonexistentscope \
  -H 'Authorization: Bearer <PUT_YOUR_ACCESS_TOKEN>'

Response:

HTTP/1.1 403

Summary

We have seen how Cloudentity can easily protect your Spring Boot API application. In addition to being an OAuth/OIDC provider Cloudentity brings in advanced external identity provider integrations and scope governance checks to ensure your application can serve users from any source and authorize them with varying conditions using policies before access token is issued to access the target service.

Updated: Jul 11, 2022