API Gateway and Cognito Auth Without v4 Signing
By: Chris Bailey
Tags: Cognito, Api Gateway, Serverless, AWS, Authentication

Why Would This Situation Occur?

First, why would you have this situation? This is fairly niche, but the particular case I’m solving is an API that is composed of an API Gateway endpoint directly injecting the data into DynamoDB — no lambdas. That said, this can apply to any API Gateway endpoint where you want authentication. This API is authenticated, to secure it in general, as well as to identify which user the data belongs to. Note that I’m also assuming the use of Cognito as your user system. If you use something else, then you may need to go the lambda authorizer route (which I didn’t want to do per above). I’ll have a future article on doing these API Gateway to DynamoDB APIs.

For most situations, you either control both the front end (code making the API call) and backend (providing the API). However, in my case I wanted to use a particular HTTP library that handles making the API calls for you, because it provides some nice functionality that I didn’t want to implement. Specifically this is the Background Geolocation library from Transistor Software. This is a great library that handles monitoring geolocation, and sending location points in batches to a backend. It has other nice features such that it handles when you have no network connectivity (queues/stores locations until it can send them), and allows various configuration. Further it’s available for native iOS and Android, Flutter, React Native, and Cordova mobile platforms. This is not likely a common need, but this article may also provide you ideas on different ways you can do auth in general or for similar problems.

When I initially started, I did location myself, and thus was able to use the AWS v4 signing process. There are decent AWS libraries for nearly any platform, and thus it was relatively simple to create signed URLs for my API. But then I switched to using the Transistor Background Geolocation library, for which I can’t control the full URL. It does allow adding headers, and you can of course set the base URL, but you don’t control the entire URL, thus can’t v4-sign it. First I’ll cover the specific solution for this, but after that I’ll cover one alternative as well. Also, as said another potential route is a lambda authorizer, which would work, but I felt meant another Lambda to maintain (and pay for) and avoided the “free” authentication mechanism you can use via API Gateway.

JWT Tokens and the Authorization Header

Tl;dr: One can authenticate to an API-Gateway endpoint using the Authorization header and a JWT token.

This sounds easy right? Well, the process is very easy, you take your JWT token for your authenticated user and pass it in the Authorization header with your API call, and API Gateway uses that to authenticate. Nice! However, those JWT tokens expire (in 1 hour), so you can’t just auth your user and be done, you need to refresh that token. This means you are now writing code to monitor and maintain that token. Because the token can change when you refresh, it also means that depending on how your HTTP library/code works, you will need to set the updated token into the configuration every time it’s refreshed.

Furthermore, you need to actually get that JWT in the first place. This is fairly easy, but I’d claim the AWS docs are not very clear on which token this is. They usually talk about the “identity token”, but they have both an ID Token and Access Token. Those docs indicate either can be used (they have different data in the JWT for each, so depends on your needs). I wound up using the ID Token. At least in the Flutter AWS library, you need to authenticate your user, get a session, then get the ID token from that, and finally a JWT from that.

To achieve this in my application, I setup a timer that is active any time a user is logged in, and that timer triggers periodically to refresh the token. The refresh is essentially the same process, where you get a session, the ID token, and then the JWT. That’s only a couple lines of code — but there wasn’t much documentation around this process in general, so hopefully this helps someone :). Every time I ensure the freshness of the token (it may or may not have expired), I update the Background Geolocation configuration with the [new] token. Further, if the user logs out, I need to remove that token from the config, and of course stop sending location events to my API.

Finally, how do you get the actual Cognito user ID into your data when using this, given API Gateway is doing the authentication from a JWT? This is provided in the data API Gateway makes available. In my case because it’s going directly to DynamoDB, I’m using a VTL template to transform the data into a suitable form for DynamoDB. As part of this, it includes the user, which I can obtain in the VTL via $context.authorizer.claims.sub .

Another Approach

Another mechanism to consider, depending on your particular needs is to not have your API do specific user authentication. Instead, you could leverage API Keys with API Gateway, to authorize the API call in general. The user ID would be provided in the data sent by the client. This technique is the likely approach when your API is simply collecting data, but you don’t control the users (e.g. your client/customer has the users, but they are using your API for whatever it provides).

This approach is much simpler, as you would just configure the HTTP library with a header for the API key. I use this with some other APIs I have, and it works great, and is really nice in terms of getting rate limiting for free, as provided by the AWS usage plans — all of which is simply configured in my serverless framework (or CloudFormation, etc.) config. No code required. Your client application still needs to be authenticating the user (and obtaining a user ID if that is different than what they use to login — i.e. the Cognito user ID vs. their email or username), but you obviously have to do that in all these cases.

What’s the downside? You are placing trust in the client that they are sending the proper user ID with the API call. This is likely fine, in that you’ve issued an API key to the client, who is presumably trusted, and presumably they wouldn’t have any reason to send incorrect or bad user IDs. Of course, if this is a public API, and if it is self-serve signup, someone could simply sign up and start spamming your API with bogus data. Or, if the API key leaked or an employee of the client put in nefarious code, or whatever, there is an opening.

You have a small protection in that API Keys can be assigned to rate limits/plans, but that doesn’t really protect you from anything but high volume attacks (doesn’t protect against sending bogus data points for a user, which could have serious ramifications, etc.). You may want to combine this with other components that will help reduce the likelihood someone could maliciously use the API if they obtained (either legitimately or not) an API key. And that is further complicated if your API is public and is fully documented.

Security is Hard

API Security is a challenge. There are so many factors involved, as well as balancing making the API overly difficult to use for your clients. AWS’ v4 signing works great if/when you can use it, and there are similar techniques employed by many public APIs that make them very secure. But there are also these times when we don’t have that ability and we have to use alternate methods. Ensuring those remain as secure as possible is tough!

This particular article covered a very narrow and specific use case. Using your own auth, or Auth0, or other such authentication systems will likely have their own issues.

I’d love to hear other ideas, downsides, problems with these approaches, etc.