?

Automated Testing With Spring Boot as an OAuth2 Resource Server

How do we isolate our automated tests when creating a Spring Boot API acting as a resource server with a third-party authentication server? Here is one method.

January 28, 2021 10 minute read

When creating an API built using Spring Boot as a resource server, it can be difficult to write automated tests with endpoints that utilize a third party authentication server. We like our automated tests to be isolated from outside influences and side effects.

When testing the web layer without the need to startup the server, we could employ the MockMvc class. However, this would not instantiate the whole context, but rather only the web layer. In this article we'll be using TestRestTemplate with a full Spring application context and a running server.

Please see the Github repository for this article to follow along.

Initial setup

Making use of the Spring Initializr site will give us a template project to start with. For the purposes of this article, we'll select a project type of Gradle Project, a language of Kotlin, and add in the Spring Web and OAuth2 Resource Server dependencies. 

Once this is done and the downloaded file unzipped, the project can be compiled and tests can be run. Before moving on to the first test, a few more dependencies are needed to get us going. To the build.gradle.kts file add in the following lines:

First, an extra property storing the Spring Cloud version we'll be using.

extra["springCloudVersion"] = "Hoxton.SR9"

Next, we'll make use of the Spring Cloud bill of materials (BOM) to help manage the related artifacts with the version property we defined above.

implementation(platform("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}"))

Lastly, we'll add in some dependencies needed for the automated tests.

testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
testImplementation("org.bitbucket.b_c:jose4j:0.7.2")

The spring-cloud-contract-wiremock library will allow us to run a WireMock server while executing tests. The jose4j library will bring in the functions needed to create a JSON Web Token (JWT) and digitally sign it to represent the JWT as a JSON Web Signature (JWS).

Configuration

We'll need to do some configuration.

Let's create a class to hold the creation and configuration of the WebSecurityConfigurerAdapter that will define the HTTP methods and URLs needing authentication and those we will allow access to without a bearer token in the HTTP request.

@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(HttpMethod.GET, "/actuator/**", permitAll)
                authorize(HttpMethod.OPTIONS, "/**", permitAll)
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {}
            }
        }
    }
}

In the code above we've configured the SecurityConfiguration class to do three things.

  1. Permit all requests to endpoints where the path begins with “actuator” and the HTTP method of GET was used
  2. Permit all requests to all endpoints when the HTTP method of OPTIONS was used
  3. Require authentication for any other requests

If we were to attempt running the provided test class of DemoApplicationTests at this point without any updates, we would see exceptions with phrases such as “Failed to load ApplicationContext” or “No qualifying bean of type ‘org.springframework.security.oauth2.jwt.JwtDecoder’ available” due to missing configuration values. So let’s resolve those.

Into the application.properties file, we'll add the following line

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://{tenant}.auth0.com/.well-known/jwks.json

In the example above the configuration will provide the endpoint that will be employed to retrieve the JSON Web Key (JWK) which will provide the public keys used to verify the JWS. The URL structure will look familiar to anyone using Auth0 as an authentication server. Another option in the Identity and Access Management (IAM) space is Okta. If searching for an open source solution then perhaps Keycloak , FusionAuth or ORY / Hydra might fit the bill.

Brief note on OAuth2

OAuth2 defines four roles we should become familiar with.

  • Resource owner: An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end user.
  • Resource server: The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens. The protected resource might be your address or similar information we would like to guard other users from being able to access.
  • Client: An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices).
  • Authorization server: The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization. The different authentication flows that can be utilized is outside the scope of this article.

For the purposes of this article, the Spring Boot API will be the resource server. An undefined third party application will be the authorization server. The client is irrelevant at this point but could be thought of as a mobile application or web application. Lastly, the resource owner would be the end user of that client.

Create an endpoint

Let's write a simple endpoint we'll be able to test against. Create a class named CustomerController and add the code below.

@RestController
@RequestMapping("/customers")
class CustomerController {
    @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
    fun getCustomers(): List<Customer> = listOf(Customer("always right"))
}

data class Customer(
    val name: String
)

In the code above, we've created a controller that should respond to requests made to /customers using an HTTP method of GET. This endpoint will respond with a list of Customer objects each containing a name property. With the configuration in the SecurityConfiguration class we know the endpoint will require authentication for the server to respond.

Writing the tests

Let's go ahead and test that.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerTests {
    @Test
    fun `should be unauthorized without bearer token`(@Autowired restTemplate: TestRestTemplate) {
        val response = restTemplate.getForEntity<List<Customer>>("/customers")

        assertThat(response.statusCode).isEqualTo(HttpStatus.UNAUTHORIZED)
    }
}

If we examine that code further, the usage of the @SpringBootTest annotation will start a Spring application context. The additional parameter of webEnvironment set to a RANDOM_PORT will cause a server to be started. In the test itself, we've injected an instance of the TestRestTemplate which we use to make a request to the endpoint we defined above. The request does not contain a token to verify the user, so therefore we are expecting a 401 Unauthorized response.

For the next test, to verify the authentication works as expected and data can be retrieved, we'll need to do some setup. Add the annotation in the code block below to the top of the tests file on the class.

@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test")

The @AutoConfigureWireMock annotation will do exactly what it sounds like and configure a WireMock instance on a random port. The @ActiveProfiles annotation will read values from an application-test.properties file we will create in the next step. The properties in the file will override the ones from the main application.properties file.

Create a file named application-test.properties at the location src/test/resources and enter the lines below.

wiremock.server.baseUrl=http://localhost:${wiremock.server.port}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${wiremock.server.baseUrl}/.well-known/jwks.json

The first line will define a property to hold the base URL with the  WireMock port provided from the wiremock.server.port property. The second line will override the setting for the endpoint hosting the JWK values the code will employ to verify the JWS value sent with the request.

Next let's create a class named JWSBuilder and copy in the code in the block below.

data class JWSBuilder(
    var rsaJsonWebKey: RsaJsonWebKey? = null,
    var claimsIssuer: String? = null,
    var claimsSubject: String? = null) {

    fun rsaJsonWebKey(rsaJsonWebKey: RsaJsonWebKey) = apply { this.rsaJsonWebKey = rsaJsonWebKey }
    fun issuer(issuer: String) = apply { this.claimsIssuer = issuer }
    fun subject(subject: String) = apply { this.claimsIssuer = subject }

    fun build(): JsonWebSignature {
        // The JWT Claims Set represents a JSON object whose members are the claims conveyed by the JWT.
        val claims = JwtClaims().apply {
            jwtId = UUID.randomUUID().toString() // unique identifier for the JWT
            issuer = claimsIssuer // identifies the principal that issued the JWT
            subject = claimsSubject // identifies the principal that is the subject of the JWT
            setAudience("https://host/api") // identifies the recipients that the JWT is intended for
            setExpirationTimeMinutesInTheFuture(10F) // identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
            setIssuedAtToNow() // identifies the time at which the JWT was issued
            setClaim("azp", "example-client-id") // Authorized party - the party to which the ID Token was issued
            setClaim("scope", "openid profile email") // Scope Values
        }

        val jws = JsonWebSignature().apply {
            payload = claims.toJson()
            key = rsaJsonWebKey?.getPrivateKey() // the key to sign the JWS with
            algorithmHeaderValue = rsaJsonWebKey?.algorithm // Set the signature algorithm on the JWT/JWS that will integrity protect the claims
            keyIdHeaderValue = rsaJsonWebKey?.getKeyId() // a hint indicating which key was used to secure the JWS
            setHeader("typ", "JWT") // the media type of this JWS
        }

        return jws
    }
}

The code above implements a Builder pattern that will be used by the tests class to create a JWS. The tests file will provide the JWK specifically using the cryptographic algorithm family of RSA in addition to the issuer and subject used with the claims. The build function will create a set of claims, with an expiration time 10 minutes into the future, and then create a JWS using the JWK  and specifying the signature algorithm and header type.

Next we'll create an instance of the JWSBuilder and set the relevant values before all of the tests in the current tests class are run.

companion object {
    private val rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048)
    private val subject = UUID.randomUUID().toString()
    private var jwsBuilder = JWSBuilder().subject(subject)

    @BeforeAll
    @JvmStatic
    fun setUp() {
        rsaJsonWebKey.apply {
            keyId = "k1"
            algorithm = AlgorithmIdentifiers.RSA_USING_SHA256
            use = "sig"
        }

        jwsBuilder.rsaJsonWebKey(rsaJsonWebKey)
    }
}

One more step prior to being able to write the test is stubbing the endpoint in  WireMock to return the JWK we've created in our code.

@Value("\${wiremock.server.baseUrl}")
private lateinit var wireMockServerBaseUrl: String

@BeforeEach
fun init() {
    jwsBuilder.issuer(wireMockServerBaseUrl)

    WireMock.stubFor(
        WireMock.get(WireMock.urlEqualTo("/.well-known/jwks.json"))
            .willReturn(
                WireMock.aResponse()
                    .withHeader("Content-Type", "application/json")
                    .withBody(JsonWebKeySet(rsaJsonWebKey).toJson())
            )
    )
}

In the code above, we've specified that when a call to the /.well-known/jwks.json endpoint is made to the WireMock server then it will respond with the JWK we've built. The /.well-known/jwks.json endpoint matches the setting in the application-test.properties file.

Now we're ready to write the test.

@Test
fun `should be able to fetch customers with valid bearer token`(@Autowired restTemplate: TestRestTemplate) {
    val token = jwsBuilder.build().compactSerialization
    val request = RequestEntity
        .get("/customers")
        .headers { it.setBearerAuth(token) }
        .build()

    val customers = restTemplate.exchange<List<Customer>>(
        url = "/customers",
        requestEntity = request,
        method = HttpMethod.GET
    ).body

    assertThat(customers)
        .isNotNull
        .containsExactly(Customer("always right"))
}

The headers value will map where we will set the bearer token in the header field named Authentication. We'll make the same GET request as the previous test but this time providing the headers and with an expectation of a response that includes a List of Customer objects in the body.

With that, we now have a couple of tests written to verify that the lack of a token or authentication data results in an unauthorized response and that the existence of a bearer token in the request will get verified and respond with the resources expected. It's an exercise for the reader to write a test verifying that an invalid token would also result in an unauthorized response.

The Github repository contains a working example to reference.

Share this

Comments