DEV Community

Cover image for Protecting Applications with Kong security plugins and using StatsD to monitor system states — A healthy camera story
João Esperancinha
João Esperancinha

Posted on

Protecting Applications with Kong security plugins and using StatsD to monitor system states — A healthy camera story

1. Introduction

One of the issues we all face in current times when developing software is the need and the will to monitor and keep applications in check. Instead of just using metrics to study and investigate our application, we can also use them for other means such as notifications and alerts. The Kong Gateway provides many free options that we can explore in order to achieve this. In this article, I will try to demonstrate a few essential aspects of the free Kong Gateway plugins that can help us detect issues with our Java/Kotlin implementations. I will try to use the most advanced and complex ways of implementing algorithms in both languages and how Kong can help us. For this article, our real live environment is cameras protecting a prized item. In particular, we will protect a plant of the species Peperomia Obtusifolia. If you don’t know what that is, here is a photo of my own plant of this species:

blogcenter

So, why Kong, and why trying to find what it can do? The Kong framework, albeit being a paid framework if we truly want to escalate well with our product (like any other framework out there), does allow us to implement a lot in our application and provide security to it.
In the application for this example, we will also have a look at how we can easily protect, otherwise unsafe running applications with Kong. We’ll have a look at examples of how this may work without a gateway, when it’s a good idea to use it and when it potentially can be too much of a good thing.
But for now, I just want to ramble a bit about security and why security is important especially in current times for literally everyone, unless you don’t use the internet at all, have no email, no telephone and don’t use electricity.
Please note, before you read onwards, that this article, will be subject to multiple updates in the future. The repo supporting this is located on GitHub.

1.2. Security in the eyes of a private user

If we take a look at today's times, we are living in an era, where it's almost impossible not to talk about cybersecurity. Everyone is in a way affected by this. The example where the discussion mostly starts is fraud. It can take many shapes or forms. We may receive emails that may tell some story about us, promise rewards, and prizes, or tell us that we have physical issues and that we need to click on some link to avoid fines, punishment, or whatever. The sky is the limit for fraudsters and even when they realize we took notice, they still keep sending emails using evasive techniques like creating multiple subdomains, creating emails with a hash, sending subdomains with a combination of a 6-digit prefix, pretending to be the real deal using strange characters, etc. The worst example is when one gets an email that does come from a real domain, but it still is nothing but a fraud. Perhaps it came from an infiltrator in that organization, or perhaps it's a masked email. Unfortunately, when we get this kind of overflow of crazy emails, one can’t get rid of them at once just like that, because fraudsters are quite able to create anonymous emails as frequently as possible. We should keep reporting them as phishing attempts, report them to Google when they come from Gmail accounts, and block them. Sometimes though, it seems like you can’t block them, but you really always can. I have received all of the above and more but also mails without a sender or emails with a dot before the domain name. However, all emails have headers and in the headers, you can find a ton of information about where the email came from. Your email provider should provide you with an option to see the source headers. And there you’ll find where the email is coming from. An easy way to get rid of some of these emails is to let your email provider use your SPAM folder, or create redirect rules. However, the problem with that, is that we are still receiving these emails and so the criminal entity has no reason to believe that you’ll never feel enticed to react to that specific email. And if you delete that email automatically, you are still not giving any signs that you understand what fake emails are. If you block the email or the whole domain, then it’s a different story. If your email provider allows you to block emails in such a way that they don’t even enter your email inbox, then that also means that the criminal entity will get a reply that the email wasn’t delivered. There will be no indication that your email account exists, is blocking emails, or anything else. The only data they will get is just that their hacking attempt didn’t find its way into your email. What you should NEVER EVER EVER do, is engaging with the sender, tell the sender off, or take any of this into your own hands. Never do this! Every time you send your frustrations in, you are just giving extra information to the criminal entity. Always read the headers, block the sender with the email in the sender field in the headers, and use at least the phishing option to report your email. Some providers have higher levels of reporting criminal emails. And sometimes it's also a good idea to report to third parties that are responsible for cybersecurity in your own country or internationally.
Will they ever give up? Unfortunately, it can be quite difficult to make them give up. Unfortunately, Cybercrime are really on the rise, and in order to do something about this, we have to understand that this is part of our new life, accept it, and make a joint effort to stop this.
What does this have to do with this article? The underlying message is that in order to protect our application, we need to take into account that in the worst-case scenario, the threat actors can be very perseverant, and they will go down the rabbit hole and stretch everything they know, to reach their goal.
A few pieces of advice on fraudulent emails. To make sure you block the correct ones please look into the headers of your email. Your provider should have the option to do this and look for these headers in the very long text you get when you view them:

From: "possibly an actual email. this could be masked one so it's not always a good idea to block whatever is put here"
To: undisclosed-recipients:;
Subject: "whatever"
Reply-To: "this is the email you should block"
Mail-Reply-To: "this is the other email you should block"
In-Reply-To: "block this one too"
References: "whatever"
Message-ID: "whatever"
Enter fullscreen mode Exit fullscreen mode

If you need to know from which country and location where this email was sent on the last point of its trip to you then look for these headers:

Authentication-Results: spf=pass (sender IP is "this is the IP you are looking for")
Enter fullscreen mode Exit fullscreen mode

Or you can also look it up on this header:

X-Sender-IP: "this is the IP you are looking for"
Enter fullscreen mode Exit fullscreen mode

You can then pick this IP and look it up in: IP WHOIS Lookup.

An IP WHOIS Lookup determines ownership information of any IP address. Search for IP WHOIS information using the IP…www.whatismyip.com.

1.2 Security in the eyes of a public service

As of today, there are several different algorithms that allow us to create very safe application accesses. We can have security systems purely based on username/password combinations, shared secrets, tokens, up-to-integrity checks, and third-party authenticators. For good measure, we can also monitor our applications using standards that can then be used in different monitoring applications. This means that on one hand, we have to be responsible for protecting our application and gateways to it. On the other hand, we have to make sure that we can detect whenever there is a security breach. In this article, we’ll have a look at both sides of this equation.

1.3. Case

We are going to have a look, as mentioned before at monitoring how is it going with a plant. For that, we have 6 cameras, or at least 6 simulated cameras, and each is implemented with its own security algorithm. Camera 1 is running a metric system known as StatsD. The cameras are numbered from 1 to 6 and their respective security implemented security algorithms are: Basic, HMAC, JWT, ApiKey, LDAP, and OAuth2. The way this works is by running a service that is completely unprotected behind the Kong Gateway. For the purpose of this article, we aren’t going to dive into the Ingress controller or other ways to isolate our domain from the outside world. For now, we just use Kong Gateway in its simplest form. In the end, we’ll have a quick look at how we handle data with StatsD.

2. Generic overview

In order to get our system started, we’ll simply create one Spring Boot service, where we will serve a welcoming message and an endpoint that will simply give out an image. The image we are giving out and the welcome end-point is not protected at all. For this article, we are simply going to have a look at how to protect them via Kong. The following is a visual representation of what we have implemented for our case:

blogcenter

In our system, we’ll have 6 cameras. They are all implemented in the exact same way. Because its important to understand what is happening, let’s first dive into the backend code for the services. For this, we’ll cover a few main sections: the StatsD custom metrics, the Rest services implementation, the reactive WebSockets implementation, and finally the security tags related to OAuth2.

3. StatsD

3.1. Custom StatsD tags

In order to get started with StatsD, we need to understand the basics of how it works. StatsD was originally developed by Etsy and is used for many application developments where we need to monitor our applications. Many frameworks provide standard StatsD metrics, which normally are things like the time it took to make a request, the request payload, response times, and the type of requests. We are usually concerned about how responsive our application truly is, its capacity, its resiliency, and how reactive it is. StatsD was designed precisely to allow us, using a standard, to perform these kinds of measurements. There are records on the web about it starting at around 2011, but odds, are that it was already in the making a few years prior to that. After that, major competitors like Kong and Datadog created plugins and adaptors in order to read this data and present it in the form of graphs. And this is also because, StatsD, being created as a standard, also allows us to create new metrics in a standard way. This is also a focal point in this article, before moving on to security. Kong provides two plugins in order to digest StatsD information. It has a StatsD regular plugin and it has a StatsD advanced plugin. The latter is used for more advanced enterprise applications. The first is the one we going to be looking at. But before any of that, let’s have look at the essential parts to make StatsD a reality purely via Spring and using Graphite as a service to listen to this data:

blogcenter

Graphite opens port 8085 for its web interface. It is not the most glamorous webpage ever, but the service has a great startup time and does not consume a lot of resources. So It works brilliantly for demonstration purposes. The flow starts on each individual camera, where we use a micrometer library to emit our Prometheus data in a StatsD-recognized format. The format we choose for our project is Etsy which is accepted by Graphite. So, how do we do this? First, we add the necessary libraries:

<dependency>
   <groupId>io.micrometer</groupId>
   <artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
   <groupId>io.micrometer</groupId>
   <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
   <groupId>io.micrometer</groupId>
   <artifactId>micrometer-registry-statsd</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

This enables us to configure our SringBoot service:

management.endpoints.web.exposure.include=prometheus,health
management.endpoint.health.show-details=always
management.statsd.metrics.export.enabled=true
management.statsd.metrics.export.host=localhost
management.statsd.metrics.export.port=8125
management.statsd.metrics.export.flavor=etsy
Enter fullscreen mode Exit fullscreen mode

In this configuration, we are saying SpringBoot to open up the Prometheus and the health endpoints. These two are very important. The Prometheus endpoint is where we are going to expose our custom metrics. Showing health details can be important because the generic health actuator endpoint is usually a composition of different health endpoints. In our case, we also have the database endpoint and so it is always good to have a full description of what is happening should our service not become healthy. Further, we enable StatsD and what this does is immediately allow our SpringBoot service to send StatsD data to a destination. We define that destination in the next 2 steps where we say that host is localhost and the port is 8125. This port is not the GUI port, which we have seen before to be 8085. The field flavor is just a fun way to call the protocol which our metric data is being sent with. There are other flavors as alternatives to Etsy. Finally, we define the frequency of our push to Graphite and the polling period to any gauges we have implemented. Should any of the Gauges present a different value than the previous reading, the StatsD exporter will resend all data back to Graphite once again.
Now that we have everything configured, we can look at the code:

@Configuration
class MetricsConfiguration(
    val cameraService: CameraService,
    @Value("\${hc.camera.number}")
    val cameraNumber: Long,
) {
    private val last10FileDeltaNSReading: MutableList<Double> = mutableListOf()
    /**
     * You should see all fails happening every 10 seconds if your polling rate is every 10 seconds
     * For every 10 seconds you'll get all fails in that period
     */
    @Bean
    @Qualifier("counterMetric")
    fun counterMetric(meterRegistry: MeterRegistry) = Counter.builder("camera.fail.count")
        .tag("type", "fail_count")
        .description("The number of anomalies detected by the camera")
        .register(meterRegistry)
    /**
     * You should see all fails happening every 10 seconds if your polling rate is every 10 seconds
     * For every 10 seconds you'll get all heartbeats in the period
     */
    @Bean
    @Qualifier("heartBeats")
    fun heartBeatCounter(meterRegistry: MeterRegistry) = Counter.builder("camera.heartbeats")
        .tag("type", "heart_beats")
        .description("The number of heartbeats measured per time and counts 1 per second")
        .baseUnit("beats")
        .register(meterRegistry)
    /**
     * Measures the time it takes to read an image.
     * This is purely to illustrate how can we inject our own custom metrics in StatsD.
     */
    @Bean
    fun gaugeMetric(meterRegistry: MeterRegistry) =
        Gauge.builder("camera.image.read.time", last10FileDeltaNSReading)
        { last10Records ->
            logger.debug("$last10FileDeltaNSReading")
            measureNanoTime { runBlocking { cameraService.getImageByteArrayByCameraNumber(cameraNumber) } }.toDouble()
                .let { record ->
                    last10Records.add(record)
                    if (last10Records.size == 11) {
                        last10Records.removeFirst()
                    }
                    logger.info("Refreshed ${last10Records.size} metrics. Last value read is ${last10Records.last()} ns")
                    record
                }
        }
            .tag("type", "image_read_ns")
            .description("Time to read one image from camera in ns")
            .baseUnit("ns")
            .register(meterRegistry)
}
Enter fullscreen mode Exit fullscreen mode

For this case, we are creating a bean called counterMetric of type Counter. The function of this type of metric is to simply count the occurrences of some event. We can do this programmatically, and we’ll see how this occurs later on. For now, we just need to bear in mind that bean counterMetric and bean heartBeatCounter will respectively count the number of failed attempts to access an OAuth endpoint with the wrong scope and the other will count 1 heartbeat per second. Finally, and purely because we want to make a strange metric in our own way, we create a gaugeMetric type Gauge. This gauge will just read the time it takes to read an image from a camera. Gauge is a type of metric, that, unlike counter, it measures the actual value read at one point in time. It is important to be conscient at this point that a Gauge processes information at specific points that we define. We will do this programmatically. So maybe to better make it clear the difference between counter and gauge, we can say that countermeasures the number of times an event has occurred between readings. So, in our specific case, we will register 10 heartbeats per second. Our counter only knows that it counts plus 1 every second. However, when sending this information to Graphite, it will register the number of times it counted from the last point or up until that point if no reading has yet occurred. So if data is processed every 10 seconds in graphite, we’ll get 10 readings every 10 seconds. Sending data every 2 minutes does not change that. Gauge, however, registers a specific value for that occasion. It does this by keeping the data in a List. However, this list doesn’t really play a role in sending the data to StatsD. Instead, it is there if we want to use a common object to share data between readings. It works more like a bean, and we can insert whatever we want in or do whatever we want with it. The result of the function we pass in is what matters to our Gauge metric. Let’s zoom in on that:

@Bean
fun gaugeMetric(meterRegistry: MeterRegistry) =
  Gauge.builder("camera.image.read.time", last10FileDeltaNSReading)
  { last10Records ->
      logger.debug("{}", last10FileDeltaNSReading)
      measureNanoTime { runBlocking { cameraService.getImageByteArrayByCameraNumber(cameraNumber) } }.toDouble()
          .let { record ->
              last10Records.add(record)
              if (last10Records.size == 11) {
                  last10Records.removeAt(0)
              }
              logger.info("Refreshed ${last10Records.size} metrics. Last value read is ${last10Records.last()} ns")
              record
          }
  }
      .tag("type", "image_read_ns")
      .description("Time to read one image from camera in ns")
      .baseUnit("ns")
      .register(meterRegistry)
Enter fullscreen mode Exit fullscreen mode

So in our function’s receiver, we get a last10Records object, which we’ve made into a list. Then we measure how long it takes to read a camera. We have to do it in a blocking way in this case purely because we are not in a coroutine context and because we want to read how long it takes to read the image. If we do it via something else like GlobalScope.launch, we would not have a realistic idea of how long it took to read the image since, in that way, the coroutine would be spread out between threads, and it would be impossible to have a realistic idea of how bad it would get while reading an image. We add our record measurement to our list of 10 records, and we make sure that we always keep the last 10 records.
Now that we have the metrics in place, we need to see how the counters work and where we trigger the measurements. We first take a look at class CameraServiceApplication:

@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@EnableScheduling
@EnableWebFluxSecurity
@OpenAPIDefinition(
    info = Info(title = "OpenAPI definition"),
    servers = [Server(url = "\${hc.server.url}/api/v1/hc", description = "Server URL")]
)
@EnableReactiveMethodSecurity
class CameraServiceApplication(
    private val webSocketHandler: WebSocketHandler,
    @Qualifier("heartBeats")
    private val heartBeats: Counter
) {

    /**
     * You should see 10 heartbeats as a max in graphite if graphite is configure to take samples every 10 seconds.
     * For every 10 seconds you'll get 10 heart beats
     */
    @Scheduled(cron = "*/1 * * * * *")
    fun incrementHeartBeat() {
        heartBeats.increment()
    }

    @Bean
    fun webSocketHandlerMapping(): HandlerMapping? {
        val map: MutableMap<String, WebSocketHandler> = HashMap()
        map["/camera-states-emitter"] = webSocketHandler
        val handlerMapping = SimpleUrlHandlerMapping()
        handlerMapping.order = 1
        handlerMapping.urlMap = map
        return handlerMapping
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            runApplication<CameraServiceApplication>(*args)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What is important here to understand and that it really stands out is that we are using a Spring Scheduler to invoke the increment function of our heartBeats Counter.
So now we know what happens to the heartBeats counter and the gaugeMetrics. So what about the fail counter? This is located at a service level. In our endpoint, we need to protect our entry points, and we do so like this:

@GetMapping(<value = ["/scopes/admin"])
@ResponseBody
@PreAuthorize("@securityService.checkAdminHeader(#scope)")
fun findScopeAdminInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeAdminInfo()

@GetMapping(value = ["/scopes/observer"])
@ResponseBody
@PreAuthorize("@securityService.checkObserverHeader(#scope)")
fun findScopeObserverInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeObserverInfo()

@GetMapping(value = ["/scopes/visitor"])
@ResponseBody
@PreAuthorize("@securityService.checkVisitorHeader(#scope)")
fun findScopeVisitorInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeVisitorInfo()

@GetMapping(value = ["/scopes/researcher"])
@ResponseBody
@PreAuthorize("@securityService.checkResearcherHeader(#scope)")
fun findScopeResearcherInfo(@RequestHeader("x-authenticated-scope") scope: String) = findScopeResearcherI>nfo()
Enter fullscreen mode Exit fullscreen mode

Here we are using PreAuthorize annotations to indicate an Expression Language (EL) condition to validate that we can make the rest call. These annotations are activated in the Application Running class:

@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@EnableScheduling
@EnableWebFluxSecurity
@OpenAPIDefinition(
    info = Info(title = "OpenAPI definition"),
    servers = [Server(url = "\${hc.server.url}/api/v1/hc", description = "Server URL")]
)
@EnableReactiveMethodSecurity
class CameraServiceApplication(
    private val webSocketHandler: WebSocketHandler,
    @Qualifier("heartBeats")
    private val heartBeats: Counter
)
Enter fullscreen mode Exit fullscreen mode

So going back to our methods, we see that they all use a component called securityService. Essentially this is a component that is also a Service and we are simply calling a method with it called checkHeader. In this method we check that the header matches our scope expectation and then inside we make our counter go up 1 unit:

private fun checkHeader(scope: String, expectedScope: String) =
  logger.info("Accessing with scope {}", scope).let {
      when (scope == expectedScope) {
          true -> true
          else -> counterMetric.increment().let { false}
      }
  }
Enter fullscreen mode Exit fullscreen mode

We will see further on that Gauges are cumulative, but for now this is all the basic information we need to know about a possible custom StatsD configuration.

3.2. StatsD via Kong

Custom tags are a great way to send metrics to Graphite, and using micrometer like a mechanism to trigger that is quite easy to do. However, in most cases, and as I mentioned before, we don’t really need manually, or custom-created metrics. This is really where StatsD becomes so relevant. Kong provides many functions with the standard StatsD plugins. So let’s have a look at how that works. We’ll use the deck container to make the Kong update possible. Further down this article, we’ll look into the details of what the deck container is about. For now, let’s just bear in mind that the deck container allows reloading and updating our konh.yml file. We’ll only configure StatsD to work with camera 1. We first need to configure a route:

- host: camera-1-service
  name: camera-1-service
  path: /api/v1/hc
  port: 8080
  protocol: http
  routes:
  - name: camera-1-service-route
    paths:
    - /camera-1-service/api/v1/hc
    strip_path: true
Enter fullscreen mode Exit fullscreen mode

So this just means that we’ll be able to access a service running locally on host camera-1-service, on port 8080, with path /api/v1/hc. This location is then made accessible via port 8000 (the Kong gateway port for our example) on the path /camera-1-service/api/v1/hc. This ensures that when we try to access our camera-1-service, we will always have to go through the Kong gateway. This mechanism isn’t that much different than a simple proxy. However, because Kong is a gateway platform, it is highly manageable, and we can make changes in real-time. One of the things that we can do is directly add the StatsD plugin:

- config:
    host: graphite
    metrics:
    - name: request_count
      sample_rate: 1
      stat_type: counter
    - name: request_size
      sample_rate: 1
      stat_type: histogram
    - name: response_size
      sample_rate: 1
      stat_type: histogram
    port: 8125
    prefix: statsd
  enabled: true
  name: statsd
  service: camera-1-service
Enter fullscreen mode Exit fullscreen mode

In this case, we are using out-of-the-box metrics. The request_count, the request_size, and finally the response_size. Data will be sent every second to Graphite. As we have seen before, this is equivalent to the polling interval, and it works in exactly the same way.

4. Common Camera Functions

As I mentioned before, all cameras are going to start out of the same deployment package. They have common functionalities and others that are specific to certain protocols. For now, let’s just have a look at the functionalities we can use for all our cameras.

4.1. Welcoming message

The welcoming message is just a text that is useful for testing what we are going to have a look at. It is as simple as a GET endpoint:

@GetMapping
fun welcome() = "Welcome to Healthy cameras!"
Enter fullscreen mode Exit fullscreen mode

Next to it, we have the endpoint responsible for retrieving the camera image for a given time:

@GetMapping(value = ["/camera"], produces = [MediaType.IMAGE_JPEG_VALUE])
@ResponseBody
suspend fun getImageProtected(): ByteArray? = withContext(Dispatchers.IO) {
  cameraService.getImageByteArrayByCameraNumber(cameraNumber)
}
Enter fullscreen mode Exit fullscreen mode

And its implementation here:

@Service
class CameraService(
    @Value("\${hc.camera.bank}")
    val bank: String
) {
    suspend fun getImageByteArrayByCameraNumber(cameraNumber: Long) =
        Path.of(System.getProperty("user.dir"), "${bank}/camera$cameraNumber")
            .takeIf { it.exists() }
            ?.let { resource ->
                val allImages =
                    Files.walk(resource).use { paths ->
                        val filter = paths
                            .sorted()
                            .filter { it.name.endsWith("jpg") }
                        filter.toList()
                    }
                if (allImages.size > 0) {
                    val countImages = allImages?.size ?: 0
                    val delta = (10 / countImages.toDouble())
                    val currentMinute = findCurrentMinute()
                    val index = (((currentMinute + 1) / delta).toInt()).absoluteValue - 1
                    allImages?.get(if (index == -1) 0 else index)?.let { resource.resolve(it.name) }
                } else return null
            }?.readBytes()

    fun findCurrentMinute() = LocalDateTime.now().second.toString().last().digitToInt()
}
Enter fullscreen mode Exit fullscreen mode

Generally speaking, once we are authenticated, we can get an image at a point in time. Since this exercise is just a simulation, we will get a new image according to the matching fraction of 10 seconds. This way we get a feeling of movement when we refresh the front-end page.
The front end will connect to the backend regardless of whether the user is authenticated or not. Here we will be sending status data in real-time to the front end. This will be done via Websockets:

@Component
class ReactiveWebSocketHandler(
    val healthIndicator: PingHealthIndicator
) : WebSocketHandler {
    override fun handle(webSocketSession: WebSocketSession): Mono<Void> =
        webSocketSession.send(fluxIteration.map { payload: String? -> webSocketSession.textMessage(payload ?: "") })
            .and(webSocketSession.receive().map { obj: WebSocketMessage -> obj.payloadAsText }.log())

    private val eventFlux = Flux.generate { sink: SynchronousSink<String?> ->
        Event(
            id = randomUUID().toString(),
            dateTime = now().toString(),
            status = healthIndicator.health().status
        ).let { event ->
            try {
                sink.next(objectMapper.writeValueAsString(event))
            } catch (e: JsonProcessingException) {
                sink.error(e)
            }
        }
    }
    private val fluxIteration = Flux.interval(Duration.ofMillis(1000L)).zipWith(
        eventFlux
    ) { _: Long, event: String? -> event }
}
Enter fullscreen mode Exit fullscreen mode

5. Authentication protocols

5.1. Basic Auth

Of all the 6 protocols to access the cameras we are going to have a look at, this is probably the most unsafe one to use. The way the message flow works can be seen in the following diagram:

blogcenter

In any case, we know that our camera isn’t originally protected and that we, for some odd reason, want to implement the most simple, and of course, most unsafe way to access it via a simple username and password. If your command line contains the base64 command, we can already try and do a simple test. Let’s say that I’m user Lucy and my password is winkelallemaal. We also assume that this credential is already configured to work in the system. With basic auth, all we have to do is to send Lucy:winkelallemaal to the server with your request. However, we do not send it blank. We use the base64 encoding version of it. This would be done via the command line in the following way:

echo ‘Lucy:winkelallemaal’ | base64
Enter fullscreen mode Exit fullscreen mode

The result will be something like this:

THVjeTp3aW5rZWxhbGxlbWFhbAo=
Enter fullscreen mode Exit fullscreen mode

So now this just looks like some cyphered thing and we send it this way through the wire in the Authorization header:

Authorization: basic THVjeTp3aW5rZWxhbGxlbWFhbAo=
Enter fullscreen mode Exit fullscreen mode

The issue with this security protocol arises when for one reason or another other someone grabs hold of the token. In that case, a simple decode operation will do to retrieve the username and password:

echo THVjeTp3aW5rZWxhbGxlbWFhbAo= | base64 — decode
Enter fullscreen mode Exit fullscreen mode

And the result will be:

Lucy:winkelallemaal
Enter fullscreen mode Exit fullscreen mode

But of course, we want to learn how this works with Kong and this is actually a great start to learning at least about the existence of these 6 security protocols.
In our Kong.yaml file we need to add the route to our camera-1-service and also create a plugin associated with it. For the path we can use the following:

- host: camera-1-service
  name: camera-1-service
  path: /api/v1/hc
  port: 8080
  protocol: http
  routes:
    - name: camera-1-service-route
      paths:
        - /camera-1-service/api/v1/hc
      strip_path: true
- host: camera-1-service
  name: camera-1-open-service
  path: /api/v1/hc/actuator
  port: 8080
  protocol: http
  routes:
    - name: camera-1-service-open-route
      paths:
        - /camera-1-service/api/v1/hc/actuator
      strip_path: true
- host: camera-1-service
  name: camera-1-open-service-webjars
  path: /api/v1/hc/webjars
  port: 8080
  protocol: http
  routes:
    - name: camera-1-service-open-webjars
      paths:
        - /camera-1-service/api/v1/hc/webjars
- host: camera-1-service
  name: camera-1-open-service-v3
  path: /api/v1/hc/v3
  port: 8080
  protocol: http
  routes:
    - name: camera-1-service-open-v3
      paths:
        - /camera-1-service/api/v1/hc/v3
      strip_path: true
- host: camera-1-service
  name: camera-1-socket-service
  path: /api/v1/hc/camera-states-emitter
  port: 8080
  protocol: http
  routes:
    - name: camera-1-service-socket-route
      paths:
        - /camera-1-service/api/v1/hc/camera-states-emitter
      strip_path: true
Enter fullscreen mode Exit fullscreen mode

For camera 1 we define 3 routes. The first is unique to camera 1. The other two are routes that we are going to replicate for all cameras. We are going to protect route 1 because that is where we’ll get the camera info and current image. The other 2 routes are the actuator and WebSockets routes. It is important that they stay open and unprotected so that we can check in real-time the state of the camera.
In order to protect the first route, we create a plugin configuration for the Basic Authentication:

- config:
    hide_credentials: true
  enabled: true
  name: basic-auth
  service: camera-1-service
Enter fullscreen mode Exit fullscreen mode

When we create this, we have already protected our route, but we still do not have any users. In order to make that configuration, we can simply run a few commands, which can run via a GUI or in our case, via cUrl commands:

curl -d "username=camera1&custom_id=CC1" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera1/basic-auth \
  --data "username=cameraUser1" \
  --data "password=administrator"
Enter fullscreen mode Exit fullscreen mode

In the first command, we create a consumer. In the second, we define a username and password for that same user.
This is the simple way to configure Basic Auth with this plugin.

5.2. HMAC

HMAC is an extraordinary algorithm that is based on location, timestamp a secret, and a choice of protocol. Sending data via HMAC is as simple as described in the following diagram:

blogcenter

We can see that it follows exactly the same flow as the Basic Auth protocol. However, its token is not possible to unencrypt. It is created in one way and furthermore, it depends on the location where it has been generated and the path for which it has been specifically generated. This means that if a hacker grabs the HMAC token, they won’t be able to use it anywhere even though it's a valid token. Using the same secret or private key on both ends is a concept known as symmetric key encryption. This algorithm is frequently used in M2M (machine-to-machine) connections and file transfers. The idea is to authenticate and authorize data transfers between parties in a secure way. The more complicated the secret the more difficult it will be to guess which one we are using. As we have seen before, this is why in some cases, we use the PEM private key. For our case though, and to remain minimalistic in the use of HMAC, we’ll just use dragon as our secret.
To make this configuration in Kong we first, as always need to create the routes. For this case let’s just look at the camera 2 route:

- host: camera-2-service
  name: camera-2-open-service-v3
  path: /api/v1/hc/v3
  port: 8080
  protocol: http
  routes:
    - name: camera-2-service-open-v3
      paths:
        - /camera-2-service/api/v1/hc/v3
      strip_path: true
Enter fullscreen mode Exit fullscreen mode

And now let’s protect it with HMAC:

- config:
    hide_credentials: true
  enabled: true
  name: hmac-auth
  service: camera-2-service
Enter fullscreen mode Exit fullscreen mode

With this configuration, we are protecting camera 2, but for now, it is not accessible. To make it accessible we need to issue a few commands:

curl -d "username=camera2&custom_id=CC2" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera2/hmac-auth \
  --data "username=cameraUser2" \
  --data "secret=dragon"
Enter fullscreen mode Exit fullscreen mode

For this case, we also need a consumer. In this case, we also create a username for it, but instead of a password, we create a secret
We have not seen yet how exactly the algorithm works, but that is also not the purpose of this article. What actually is the purpose of this article is to show you what variables we need to create a minimally acceptable HMAC token. For this it is probably better to jump into an algorithm created with crypto:

  createCamera2HmacHeaders = (method: string, path: string): Partial<any> => {
  const username = 'cameraUser2', secret = 'dragon', algorithm = 'hmac-sha256';
  const currentDateTimeUtc = new Date().toUTCString();
  const digestBodyHeader = `SHA-256=${crypto.createHash('sha256').digest('base64')}`;
  const signingString = `x-date: ${currentDateTimeUtc}\n${method} ${path} HTTP/1.1\ndigest: ${digestBodyHeader}`;
  const signature = crypto.createHmac('sha256', secret).update(signingString).digest('base64');
  const authorization = `hmac username="${username}", algorithm="${algorithm}", headers="x-date request-line digest", signature="${signature}"`;
  return {
    'Digest': digestBodyHeader,
    'Authorization': authorization,
    'X-Date': currentDateTimeUtc,
    'Content-Type': 'application/json',
  };
}
Enter fullscreen mode Exit fullscreen mode

Let’s go through roughly what we intend here. When we try to connect to camera 2, we need to create an HMAC token. For that, we pass in the path we are trying to access and the method with which we want to perform the request. These are only GET requests in this project, but they can be applied to any method. We start out by initializing variables that are important for our algorithm: username, secret, algorithm, and current date. In our HMAC header, we need to include a Digest header. That header is created by providing a text composed of "SHA=256=base64Hash". For now, we just need to know that this is needed for the algorithm to work.

blogcenter

So essentially for this implementation of the HMAC algorithm, we need 4 basic elements in our header. The digest, which contains encrypted information about the algorithm we are using, authorization, which is an encrypted string containing different elements, the currentDate, and finally just a Content-Type header which determines the type of content we are going to send through the wire. All four headers are very important but the most extensive one is the Authorization, which has the following form:

Authorization : hmac username="cameraUser2",algorithm="hmac-sha256",headers="x-date request-line digest",signature="<signature>"
Enter fullscreen mode Exit fullscreen mode

Sending requests through the wire this way means that each request can be sent with a new HMAC header. And the point is that, since we can do that, then why not do that? HMAC can be a heavy algorithm to process. We sacrifice performance in order to achieve optimal security.
For this camera, we’ve only scratched the surface of how HMAC works. We are only making get requests and none of them has a payload to encrypt. Although HMAC is specifically designed to protect outbound data, our goal in this article is simply to have it configured and understand a bit about how it works.

5.3. JWT

JWT tokens are created usually as a token that the user can add to their requests in order to authenticate and authorize the use of certain methods. JWTs can be created however we want. It relies on the principle of asymmetric key encryption. The encryption happens on the signature level. Normally with JWT we only need a token. A token may have an expiry date and it contains all sorts of information about the user logged on and the user permissions. A JWT token, pretty much like basic authentication, does not hide a lot. It just Invalidates if the request is accompanied by an invalid token. A JWT token is composed of a header, a payload, and a signature. The header and the payload are base64 encoded, while the signature is a function of the header and the payload. In this function, we use a private key to sign our token. If it's not correctly signed, then our application will reject it. The validation of the token needs a public key. No password is necessary for this process, although it may be used in order to request a new JWT token. Essentially this is how it works:

blogcenter

Using the Kong plugin we are then able to implement this workflow:

blogcenter

This is a very simple workflow where we make requests to the Kong Gateway purely by adding a token like this one:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0.R4S8yV_CPJRvJ2dAX_56HfNUZrbKr7Jt4AxEmsqqx8w
Enter fullscreen mode Exit fullscreen mode

Later on, we’ll see in our app how and when Kong can return these tokens to us. For now, let’s have a look at JWT.io and see what deciphering what can be decyphered in this token can tell us:

blogcenter

If you are one of these people very sensitive to red-colored messages, the first thing you probably realize when checking this token is that JWT.io, is telling us that the token is invalid. We did not insert our signature into the Verify Signature box. Since we have not given a secret for JWT.io to be able to validate, it just says that the token is invalid. But let’s have a look at the header and at the payload. We were able to decipher these two and the reason being is that they are just base64 encrypted. Let’s decipher this. At this point, you already have seen how it works. We just issue a few commands:

echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 --decode
Enter fullscreen mode Exit fullscreen mode

And the result is:

{"alg":"HS256","typ":"JWT"}
Enter fullscreen mode Exit fullscreen mode

What this tells is that this is a basic JWT token implemented with the algorithm HS256, which is used to verify the signature we are sending.
Now let’s check the body:

echo "eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0" | base64 --decode
Enter fullscreen mode Exit fullscreen mode

The result is:

{"iss":"nSD7ZSqSwSx207vR5XxiocTUZhsB2QxV","expiresIn":"12h","algorithm":"HS256"}
Enter fullscreen mode Exit fullscreen mode

Iss is the issuer. This issue can be specified and if we don’t, Kong will give us a random one. The issuer is an essential part of the signature, although it is not protected. For every time we ask for a JWT a new secret is generated. The secret for this run was: xwRs1oR22OhzBeq2hWH4NnIxdF5Jr6jv . If we input that in JWT.io we’ll be able to get validated and whatever operation we provide in the payload and in the request we are performing will be allowed to go through security. If then the authorization allows or further operations don’t matter. We are able to reach the system already, but only under the conditions we specified in the payload:

blogcenter

The secret is key here, and it's never shared. This is how we validate the request via JWT.io. But of course, here we are in possession of the private key. The algorithm on the Kong side, only validates if the request is valid with its public key.
To make this configurable in Kong we need to first configure our route:

- host: camera-3-service
  name: camera-3-open-service-v3
  path: /api/v1/hc/v3
  port: 8080
  protocol: http
  routes:
    - name: camera-3-service-open-v3
      paths:
        - /camera-3-service/api/v1/hc/v3
      strip_path: true
Enter fullscreen mode Exit fullscreen mode

And then just configure the plugin:

  - enabled: true
    name: jwt
    service: camera-3-service
Enter fullscreen mode Exit fullscreen mode

Once the service is enabled with camera 3 running we can then request our JWT token params:

curl -d "username=camera3&custom_id=CC3" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera3/jwt -H "Content-Type: application/x-www-form-urlencoded"
curl -X GET http://127.0.0.1:8001/consumers/camera3/jwt > ../e2e/cypress/fixtures/CC3KongToken.json
Enter fullscreen mode Exit fullscreen mode

These commands create a consumer called camera3. Then we generate our token with the POST request. In order to be able to use a token in the website, I create a file called CC3KongToken.json, where we can then find the issuer and the secret. The latter is also what we have referenced as the private key. It is called private because is not shared. Another camera would accept a request with the same token should it be configured with the same public key.

If we perform that last GET request, we’ll get an answer similar to this one:

{
  "data": [
    {
      "tags": null,
      "rsa_public_key": null,
      "consumer": {
        "id": "137558e2-bcc0–4fc4-a423–33a37e2d9f6e"
      },
      "key": "nSD7ZSqSwSx207vR5XxiocTUZhsB2QxV",
      "created_at": 1665153305,
      "algorithm": "HS256",
      "id": "9d6febc6–2602–42ae-a2b2–4ec3924e7bce",
      "secret": "xwRs1oR22OhzBeq2hWH4NnIxdF5Jr6jv"
    }
  ],
  "next": null
}
Enter fullscreen mode Exit fullscreen mode

To get the JWT token, we do it manually, and we have an example of that in the typescript code. We first encode the token:

encodeToken = (payload: any) => {
  const header = {
    "alg": "HS256",
    "typ": "JWT"
  };
  const encodedHeader = this.encodeObject(header);
  const encodedData = this.encodeObject(payload);
  return `${encodedHeader}.${encodedData}`;
}
Enter fullscreen mode Exit fullscreen mode

Then we sign the complete token:

signToken = (body: any, secret: string) => {
  const jwtToken: any = this.encodeToken(body);
  const signature = this.base64url(this.cryptoJS.HmacSHA256(jwtToken, secret));
  return `${jwtToken}.${signature}`;
}
Enter fullscreen mode Exit fullscreen mode

We can look at JWT on several levels. We can, in a way, consider it to be some sort of evolution to pure HMAC. Perhaps what’s not so fine with JWT is the fact that we do share some data anyways. Of course, this is nothing that a TLS tunnel cannot solve. But again, providing some information to a potential hacker is always worse than providing none. What JWT allows as an extra to the first two security protocols is the ability to attribute permissions and roles. Effectively Authorization. Perhaps one more difference we need to understand is that the token in this case is a Bearer token:

{
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuU0Q3WlNxU3dTeDIwN3ZSNVh4aW9jVFVaaHNCMlF4ViIsImV4cGlyZXNJbiI6IjEyaCIsImFsZ29yaXRobSI6IkhTMjU2In0.R4S8yV_CPJRvJ2dAX_56HfNUZrbKr7Jt4AxEmsqqx8w"
}
Enter fullscreen mode Exit fullscreen mode

The bearer token is actually part of the same problem that we’ll see with OAuth. If the token is valid then the official RFC6750 describes it as:

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can

What this means is that, as long as the token is valid, anyone in possession of it can access the protected application.

5.4. API Key

There isn’t really much to say about this type or form of authentication. API Key is a form of authentication that essentially allows a user to access an application purely on the bases of the possession of an API key. The apiKey is not sent via an Authorization header but via a header of the same name:

blogcenter

Camera 4 is protected with this security protocol. To configure it, we first configure the routes:

  - host: camera-4-service
    name: camera-4-open-service-v4
    path: /api/v1/hc/v3
    port: 8080
    protocol: http
    routes:
      - name: camera-4-service-open-v4
        paths:
          - /camera-4-service/api/v1/hc/v3
        strip_path: true
Enter fullscreen mode Exit fullscreen mode

And then we activate the apiKey plugin:

  - enabled: true
    name: key-auth
    service: camera-4-service
Enter fullscreen mode Exit fullscreen mode

In this case, we also need a consumer. To create it, we can issue these commands:

curl -d "username=camera4&custom_id=CC4" http://127.0.0.1:8001/consumers/
curl -X POST http://127.0.0.1:8001/consumers/camera4/key-auth
curl -X GET http://127.0.0.1:8001/key-auths > ../e2e/cypress/fixtures/CC4KongKeys.json
Enter fullscreen mode Exit fullscreen mode

In short, we first create a username called camera4, and then we request an apiKey. With the key-auths endpoint, we are able to make a request and as a response we’ll get a request of this form that we then are able to find in the CC4KongKeys.json file:

{
  "data": [
    {
      "ttl": null,
      "consumer": {
        "id": "99647244-bc39-4981-a187-00e2402be24b"
      },
      "created_at": 1665157122,
      "id": "7a5a39f5-011d-4557-b021-d35e6fb60030",
      "tags": null,
      "key": "87FNNInZXturFdEsal3InGMu7Me3CX5C"
    }
  ],
  "next": null
}
Enter fullscreen mode Exit fullscreen mode

In this case, the key is our apiKey. Let’s see how a request with the apiKey looks like in the typescript code:

  retrieveWelcomeMessage(input: Map<string, string>): Observable<string> {
  return this.httpClient.get(input.get("path") || "", {
    headers: this.emit({
      apiKey: input.get("key") || ""
    }),
    responseType: 'text'
  })
}
Enter fullscreen mode Exit fullscreen mode

So what’s important here to see that we are sending the apiKey in the clear through the wire:

{"apiKey":"87FNNInZXturFdEsal3InGMu7Me3CX5C"}
Enter fullscreen mode Exit fullscreen mode

An apiKey can be useful if we want to automate connections between applications. It is very lightweight and requires a few amounts of processing. However, if we choose to use this security protocol, we have to make sure that we are using it via a TLS/HTTPS connection or otherwise, an encrypted connection.

5.5. LDAP

This security protocol is something that reminds us of windows, but it’s not necessarily related intrinsically to it. In this article it is not my goal to explain how it works, but how can we get it to be configured via the Kong gateway? We are going to abstract ourselves from how LDAP implements itself and focus on its actual implementation of it. The simplest form of an LDAP flow is the following:

blogcenter

First we implement the route:

  - host: camera-5-service
    name: camera-5-service
    path: /api/v1/hc
    port: 8080
    protocol: http
    routes:
      - name: camera-5-service-route
        paths:
          - /camera-5-service/api/v1/hc
        strip_path: true
Enter fullscreen mode Exit fullscreen mode

Then we can configure the plugin:

  - config:
      attribute: cn
      base_dn: dc=example,dc=org
      header_type: ldap
      hide_credentials: true
      ldap_host: openldap
      ldap_port: 1389
    enabled: true
    name: ldap-auth
    service: camera-5-service
Enter fullscreen mode Exit fullscreen mode

For LDAP, we have something a bit different than in the other protocols. LDAP needs an external LDAP service in order to be able to work. So, essentially, using LDAP via Kong, we are not only proxying our requests to the camera but also to the authentication LDAP service. The LDAP service is what we find in docker-compose to be the openldap service.
Once having all of this in place, then we only need to send our request to the service with the authentication header next to it. In this case, we will be sending a very similar header to the one we used for camera 1 during the basic-auth algorithm, but in this case, we call it ldap:

{ Authorization: ldap YWRtaW46cGFzc3dvcmQ= }
Enter fullscreen mode Exit fullscreen mode

So this is the same principle and this is implemented in the same way as the basic-auth request:

retrieveWelcomeMessage = (input: Map<string, string>): Observable<string> => this.httpClient.get(input.get("path") || "", {
  headers: this.emit({
    "Authorization": `ldap ${(this.getCredentials(input))}`
  }),
  responseType: 'text'
})
Enter fullscreen mode Exit fullscreen mode

5.6. OAuth2

Finally, we reach OAuth2. This security algorithm is a bit complicated because it involves an external party to our application. This external party is usually what we see when we try to log on to some applications and the applications ask us to log in via Google, Facebook, iCloud, GitHub, or any other authentication provider. When we log in through the authentication provider for the first time it may ask us if we want to provide our data to any of them like our name and email, when we say yes and log in, it only means that the authentication was successful and furthermore, our application has registered us with the data provided by the authentication provider. Following that, we can successfully log in. For this part of this article, we will skip the registration process and just use an authentication provider as if we were already registered. Authentication providers can be implemented in multiple ways and that alone is something way out of the scope of this article. However, I wanted to create an example with something so that we could see OAuth2 running locally. So I’ve created a simple SpringBoot service secured with Basic Auth. This, of course, is just used as a simulation of the real thing. The idea is that, after we authenticate, the service will look for the user in the database, fetch the user’s scope and use it in order to request a bearer authentication header to the camera-6-service. This bearer token can then be used to access the camera 6 service via HTTPS. The following data flow is an illustration of the whole process:

blogcenter

In order to configure this we first need to define the route for camera 6:

  - host: camera-6-service
    name: camera-6-service
    path: /api/v1/hc
    port: 8080
    protocol: http
    routes:
      - name: camera-6-service-route
        paths:
          - /camera-6-service/api/v1/hc
        protocols:
          - https
        strip_path: true
Enter fullscreen mode Exit fullscreen mode

And then finally we can configure the OAuth2 plugin. For OAuth2 I’ve decided just protect the route via just curl commands:

curl -X POST http://127.0.0.1:8001/consumers/ \
  --data "username=camera6" \
  --data "custom_id=CC6"
curl -X POST \
  --url http://127.0.0.1:8001/services/camera-6-service/plugins/ \
  --data "name=oauth2" \
  --data "config.enable_password_grant=true" \
  --data "config.enable_client_credentials=true" \
  --data "config.scopes=admin" \
  --data "config.scopes=observer" \
  --data "config.scopes=visitor" \
  --data "config.scopes=researcher" \
  --data "config.mandatory_scope=true" \
  --data "config.enable_authorization_code=true" > ../e2e/cypress/fixtures/CC6KongProvOauth2.json
cp ../e2e/cypress/fixtures/CC6KongProvOauth2.json ../cameras-auth-service/target/CC6KongProvOauth2.json
curl -X POST http://127.0.0.1:8001/consumers/camera6/oauth2 \
  --data "name=Camera%20Application" \
  --data "client_id=CAMERA06CLIENTID" \
  --data "client_secret=CAMERA06CLIENTSECRET" \
  --data "redirect_uris=https://127.0.0.1:8443/camera-6-service/api/v1/hc" \
  --data "hash_secret=true" > ../e2e/cypress/fixtures/CC6KongOauth2.json
curl -X GET http://127.0.0.1:8001/oauth2_tokens/
Enter fullscreen mode Exit fullscreen mode

So we first create a consumer, then we configure the OAuth2 with scopes admin, observer, visitor, researcher, and then finally we configure the consumer with its own client_id, client_secret and redirect_uris. Files CC6KongProvOauth2.json and CC6KongOauth2.json are created because we need the data generated with these commands to run our tests.
Now we can have a look at our authentication provider. In the project, it is called cameras-auth-service. This service is connected to an SQL database containing the following data:

INSERT INTO hc_user (username, password_hash, roles) VALUES ('admin', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'admin');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('officer', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'observer');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('edwin', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'visitor');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('johannes', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'researcher');
INSERT INTO hc_user (username, password_hash, roles) VALUES ('lucy', '$2a$08$SwDEVbemhCUyX4U.c5sZi.oIlC4Yiy8Y7QORFAisaBtO9Nbm3dHQq', 'visitor,researcher');
Enter fullscreen mode Exit fullscreen mode

The field roles are actually the scope. All the users have the same password and the password is admin. It is never enough to keep reiterating that admin is ok to use as a password for demo purposes ONLY. Never use this kind of password in your own account.
To retrieve the authorization bearer token, there are two ways to do it. We are going to do it by first retrieving an access code and then using that to get the access token. This is implemented in this Kotlin code:

fun createToken(
    principal: UsernamePasswordAuthenticationToken, ctr: ClientTokenRequest
): Mono<ResponseEntity<BearerTokenEnriched>> = principal.authorities.map { it.authority }[0].let { scope ->
    webFluxClient.post().uri(authUrl).header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE)
        .accept(APPLICATION_JSON).body(createAuthFormRequestBody(scope))
        .retrieve().bodyToMono(ResAuthorizeBody::class.java).map { authorizeBody ->
            logger.info("Response redirect uri: ${authorizeBody.redirectUri}")
            logger.info("Input redirect uri: ${ctr.redirectUri}")
            if (validate) {
                authorizeBody.validate(ctr)
            }
            webFluxClient.post().uri(tokenUrl).body(
                createTokenFormRequestBody(scope, authorizeBody)
            ).retrieve().bodyToMono(BearerToken::class.java).map { bearerToken ->
                bearerToken.enrich(authorizeBody.redirectUri)
            }
        }
        .flatMap { it }
        .map {
            ResponseEntity
                .status(HttpStatus.OK)
                .header("Authorization", "bearer ${it.accessToken}")
                .location(URI.create(it.redirectUri))
                .body(it)
        }
}
Enter fullscreen mode Exit fullscreen mode

If we zoom in on the way the first request is made, we can see that we need to create a special token, where we need the clientId, the user’s scope, the provisionKey (this is provided by Kong), the authenticatedUserId and the responseType :

private fun createAuthFormRequestBody(scope: String) = BodyInserters.fromFormData(
    AuthorizeBody(
        clientId = clientId,
        scope = scope,
        provisionKey = provisionKey,
        authenticatedUserId = authenticatedUserid,
        responseType = responseType
    ).toMultiValueMap()
)
Enter fullscreen mode Exit fullscreen mode

This first request will provide us with the access code which we then can use to make the request that will give us the authorization bearer code

private fun createTokenFormRequestBody(
    scope: String,
    authorizeBody: ResAuthorizeBody
) = BodyInserters.fromFormData(
    TokenRequest(
        clientId = clientId,
        clientSecret = clientSecret,
        authenticatedUserid = authenticatedUserid,
        scope = scope,
        grantType = grantType,
        code = authorizeBody.redirectUri.split("=")[1]
    ).toMultiValueMap()
)
Enter fullscreen mode Exit fullscreen mode

The result of signing in to the auth app on http://localhost:8000/cameras-auth-service/api/v1/cameras/auth/login will be this:

{
  "refresh_token": "zy2hwv8ov90e5XWRXmbNxZ9NiITMkRYG",
  "access_token": "se4rpVnIofug611XgoqYwAmV7FSTwoMy",
  "expires_in": "7200",
  "token_type": "bearer",
  "redirect_uri": "https://localhost:8443/camera-6-service/api/v1/hc?code=HGsfiB63XY2MyoX7YZk42ryLKNchucyP"
}
Enter fullscreen mode Exit fullscreen mode

So this is a set of data necessary to perform our first OAuth2 request to our camera.
Sending the data with the bearer token will give us access to the camera:

{
  "Content-Type": "application/text",
  "Authorization": "Bearer kY0FMKviJgHQTuSjEE9ago1CSz5of8WA"
}
Enter fullscreen mode Exit fullscreen mode

What’s inside the Bearer token is not readable to anyone except our camera. Passing this through to the back end will be easy to make. When we access our OAuth2 application we then get access to header x-authenticated-scope. We know that when we pass through the Kong gateway, we are already in the clear to access our application. Kong provides us with not only this header but also x-authenticated-userid, x-consumer-username, and x-credential-identifier . This is all of what we above close to the beginning of this article.

6. Running the demo

To run the demo please use the following command:

make dcup-full-action
Enter fullscreen mode Exit fullscreen mode

When running, just go to http://localhost:8000

blogcenter
blogcenter

7. Conclusion

What we have seen in this article is that by using an application Gateway, we can control almost any aspect facing the application. In this article, we have only discussed the possibility of interpreting and reading metrics, but there are lots of other possibilities.
In any case, we also ended up seeing how Basic Auth can work very well for simple demo applications. This is what we did for the Authentication provider in the OAuth2 case for example. We now know that Basic-Auth can be very unsafe. We are essentially sending username and passwords through the wire without any active protocol to protect this pair. We could argue that a TLS tunnel would eventually solve this problem. But eavesdropping in a TLS tunnel has been proven to be possible and this is a risk we do not want to take. So Basic-Auth is something definitely not to use in production.
When we looked at HMAC, we also saw a couple of fundamental benefits. It is highly encrypted and it is pretty much unbreakable even if someone gets their hand on the HMAC token gibberish. We know that HMAC is also based on the origin and from the token we can’t get the origin and so many aspects of the token aren’t really accessible to potential hackers. What can be argued as the downside of the HMAC algorithm is also that its secret is fixed and also that, it is used for permanent automated connections like M2M. We used this by providing and username and a secret only. We do not send in plain text through the wire and encryption is dependent on timestamp and location and of course the secret. But no matter how far we think this through, the backend will recognize this request, check that its integrity is ok and that the user is authorized purely based on that secret. Getting that secret from the token may be next to impossible, but via social engineering then it becomes less clear. This is part of the reason that instead of using something like dragon as a secret, people use something they can configure and cannot remember, like for example a PEM private key. But another thing that may stand in the way of HMAC is the definition of profiles, roles, and access limitations to certain methods.
For JWT, we also saw that all we need is a secret and an issuer. This algorithm is also pretty very powerful in protecting our applications. Perhaps where it fails is that the header and the payload are always in the clear as requests. This means that a hacker might not get a request through, but because the header and the body are always just Base64 encoded, the decode operation is always possible. The body is in any case very handy because there we can define, for example, roles that the user may have, but again, if the roles are exposed, then a hacker already has that piece of information. Is that enough for a hacker to be able to wreak havoc? That alone happens not because it does not necessarily compromise the application but because it is still a piece of information that helps to increase the surface area for a possible hacker attack
Using an APIKey is an interesting concept that essentially doesn’t translate very well into attributing more security to our application key. Essentially, a consumer with an APIKey is just another user with a username and password. Only that in this case the user cannot determine the password. It’s the API that gives the user an APIKey to access the application. It can be used for pre-authentication purposes or to create another layer of deterring form of security.
LDAP, is another form of security that introduces almost the same kind of paradigms, problems, and situations that the Basic Auth provides. The difference is that LDAP manages accesses on a network level. In our case, we did not explore how it really works, but it’s just important to bear in mind that it is more complex, and it provides Authentication and Authorization capabilities. Only that for the purposes of this article it doesn’t really make a difference in terms of making an application secure, because it is still based on the principle that the user sends his password in the clear.
Finally, we have learned how OAuth2 works. This is the most advanced algorithm of the six we’ve seen so far. If we have an authorization provider that allows for OTP (One-Time-Password), MFA, SMS authentication, or any other form of safe login, then we can guarantee that the access to our application will be safe in the same way. Once we get a token from our authentication provider, we know that we can use it to access our application. Normally via OAuth, we get a screen asking for permissions. This is part of what the scopes mean. Each scope means that the user can access certain parts of the application. On the other hand, it means also that the camera-6-service will be able to access the provided user information associated with that scope.
StatsD is also a perfect example of how we can send standard statistics.
As I mentioned in the introduction, this article will be subject to more frequent reviews given its experimental nature.
I have placed all the source code of this application on GitHub.
I hope that you have enjoyed this article as much as I enjoyed writing it.
Thank you for reading!

8. References

Top comments (0)