Alexa Request Validation with Spring Cloud Gateway

Krzysztof Kocel

August 02, 2021


How employing Spring Cloud Gateway simplified our Voice applications stack.

Recently in ViacomCBS we’ve started to develop a couple of Amazon Alexa applications (called Skills). We’ve picked Kotlin, Spring Boot and Alexa Skills Kit SDK for Java (ASK SDK).

As a Skill developer you don’t need to deal with language or signal processing - you just respond to HTTP requests that are sent from Alexa. Request contains user speech converted to text and even user intention. Response contains directives for Alexa device - e.g. playing audio, video or text that would be converted to speech.

Alexa speech to HTTP request conversion

Preparations

Before being available to the public Skill needs to be certified by Amazon.

One of the certification requirements is that Skill would verify incoming HTTP requests and reject those that are not signed by Amazon. Such verification guarantees that only Alexa devices can interact with your Skill, but also renders it impossible to perform manual, automated or stress tests.

We decided to move verification logic into the Spring Cloud Gateway filter and enable it only for the production environment. Such decision saved us from having verification logic duplicated in each of our Skills and gave us possibility to perform tests on QA environment.

Fortunately for us, the ASK SDK contains classes needed to perform verification of the request from the Alexa device. It contained dependencies to servlet API, but with simple PR we’ve made it more generic and ready to use in the Spring Cloud Gateway filter.

Implementation

After digging into Spring Cloud Gateway documentation we’ve realized that we need to put verification logic into a GlobalFilter. Our filter implementation would operate before making HTTP request to the upstream Skill.

Having all prepared we started to write GlobalFilter:

class BodyGlobalFilter : GlobalFilter {

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain) =
        ByteArrayDecoder()
        .decodeToMono(
            exchange.request.body,
            ResolvableType.forClass(ByteBuffer::class.java),
            exchange.request.headers.contentType,
            null
        )
            .map { /* exchange to Alexa Request */ }
            .flatMap { it: AlexaRequest ->
                try {
                    /* Verify AlexaRequest */
                    chain.filter(exchange) // verification passed
                    Mono.empty<Void>() 
                } catch (e: SecurityException) {
                    Mono.error<Void>(e) // verification failed
                }
            }
            .onErrorResume(SecurityException::class.java) {
                exchange.response.statusCode = HttpStatus.BAD_REQUEST
                Mono.empty<Void>()
            } /* ...  */
}

We started with decoding body to ByteBuffer, then we mapped it to AlexaRequest. Such request was validated by verifiers provided by an ASK SDK library. When the request passed validation it was passed to the chain, otherwise Mono.error() was returned.

Implementation seemed to be pretty simple, but after running the test we realized that properly validated requests are not sent to the upstream Skill.

It turns out that in the reactive world body can be read only once. In our case it was already read by GlobalFilter.

We’ve found ModifyRequestBodyGatewayFilterFactory and modified it:

Now GlobalFilter implementation looks like following:

class BodyGlobalFilter(private val bodyFilter: BodyFilter) : GlobalFilter {

    private val messageReaders: List<HttpMessageReader<*>> = HandlerStrategies.withDefaults().messageReaders()

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {

        val serverRequest: ServerRequest = ServerRequest.create(exchange, messageReaders)
        val body = serverRequest.bodyToMono<ByteArrayResource>(ByteArrayResource::class.java)
        val headers: HttpHeaders = HttpHeaders.writableHttpHeaders(exchange.request.headers)
        val outputMessage = CachedBodyOutputMessage(exchange, headers) // cache body (1)
     
        return BodyInserters.fromPublisher(body, ByteArrayResource::class.java) // recreate body
            .insert(outputMessage, BodyInserterContext())
            .then(Mono.defer {
                decodeByteArray(outputMessage, headers)
                    .flatMap {
                        bodyFilter.filter(it, exchange) { // validate cached body in body filter (2)
                            // if validation succeeds pass exchange to chain (3)
                            chain.filter(exchange
                             .mutate()
                             .request(decorate(exchange, headers, outputMessage))
                             .build()) 
                        }
                    }
            })
    }

    private fun decorate(
        exchange: ServerWebExchange,
        headers: HttpHeaders,
        outputMessage: CachedBodyOutputMessage
    ): ServerHttpRequestDecorator {
        return object : ServerHttpRequestDecorator(exchange.request) {
            // ...
         
            override fun getBody(): Flux<DataBuffer> {
                return outputMessage.body // return cached body
            }
        }
    }

    interface BodyFilter {
        fun filter(body: ByteArray, exchange: ServerWebExchange, passRequestFunction: () -> Mono<Void>): Mono<Void>
    }
}

Because validation requires reading the whole body we need to recreate it using BodyInserter.

The key concept of this implementation is that the request body is being read once, cached (1) and then processed by BodyFilter (2). BodyFilter which encapsulates all the Alexa request validation logic (when the request passes validation successfully, chain.filter(exchange) is called in order to pass the request to the next filters (3)).

If you want to play around more with above solution you can check out code on GitHub: https://github.com/kkocel/spring-cloud-gateway-request-validation.

Any use case that requires request validation can be implemented on gateway level - for example perfect candidates are Google backend authentication or SafetyNet Attestation

Summary

If you are considering Spring Cloud Gateway then I suggest giving it a shot! It is a new project based on Spring 5, Spring Boot 2 and Project Reactor, and it has a vibrant community more than willing to help.