A Cognito Protected Serverless API with Golang in Minutes
The Need for Speed
Over a year ago I set out on a fun side project to build myself a solution to all the boilerplate associated with building Lambda functions. Second to that, a solution for using Go with AWS Lambda. The goal was convenience and speed.
While there are a few other frameworks, none of them seemed to capture what I was after. Don’t get me wrong, they’re great and all, but I just wanted something different. So I started Aegis.
Recent native Go support for Lambda rekindled my interest in the project; I was able to quickly ditch the Node.js shim and expand upon the framework. Enough to now call it a “framework” instead of just a tool. As I went to use it in production, I found more helpful features to add.
The latest feature added was a big one on my list — Cognito support. That is to say, interfaces and helper functions for making life easier when using Cognito.
Enough Talk, Show me the Code!
The deploy tool won’t manage Cognito user pools for you. So you’ll first want to ensure you setup a new user pool (a quick process from AWS web console). You can follow the instructions in the readme of the example. You can also check out the main.go file in there, and it’s whopping 114 lines of code! Whopping? Wait, no, that’s measly.
I’m sure you can make it less than 114, I wasn’t trying to make it the shortest thing ever, just easy to follow and deploy.
Go get Aegis setup, change to that example directory, plugin your user pool ID, etc. and deploy. The deploy took 1 minute and 32 seconds and most of that is in the upload time. You should be able to have a Cognito protected API up in less time than it takes to read this article.
We’re leveraging AWS Cognito hosted pages for registering users and logging in. Under the hood, we’re exchanging an authorization code for JWTs. Then we’re verifying the access_token. Your typical OAuth 2.0 workflow really.
Then we’re using some middleware on our event handlers to protect paths in the API. All of this occurs inside one Lambda. So we aren’t restricting access to the invocation of a Lambda, but rather preventing access to certain types of events handled by the Lambda…And we have very granular control over this from an easy to use interface.
Of course if you wanted to have a separate Lambda handling other events or APIs, you still could use the same JWTs for securing it as well.
The two meatiest pieces of code for auth here are the callback handler and the token verification function. Here’s the example callback that would be part of your application code:
Under the Hood
There’s a lot you won’t need to worry about coding too. We take the “code” and exchange it for a few tokens (id_token
, access_token
, refresh_token
). We take the access_token
and verify it. If valid, we set a Secure
HttpOnly
cookie so we can check it in our middleware later on.
CognitoAppClient
is an interface that helps us out quite a bit. Two major functions are exchanging authorization codes for JWTs and then verifying JWTs. It also figures out AWS Cognito hosted page URLs for you as well. It just saves you from a lot of boilerplate.
Two heroes here are the https://github.com/dgrijalva/jwt-go and https://github.com/lestrrat/go-jwx packages. They do most of the heavy lifting when it comes to parsing and verifying.
I think you can figure out how the middleware provided by Aegis works now too. It just takes the incoming API Gateway HTTP request (event, remember Lambda passes events as map[string]interface{}
) and finds the access_token
cookie to verify. It returns an error if not valid. Otherwise it lets the flow of execution continue into the handler.
You can also use your own middleware of course.
Even More Helpers!
Another thing Aegis provides are helpers to get cookies. So if you did want to write your own middleware, this should help you out.
While an API Gateway HTTP request doesn’t actually come in as map[string]interface{}
because Amazon’s Go Lambda package marshals it to an APIGatewayProxyRequest
, it still doesn’t make getting the cookies much easier. If you’re familiar with Go’s http
package, you’ll know getting cookies is very simple.
Part of Aegis’ goal is to be conventional.
So Aegis provides the same helper function to get cookies. Either all cookies or a single cookie by name. The way it does this is by creating a fake http.Request
from that Lambda event and then proxying the very same functions.
Now you can call req.Cookie("access_token")
for example.
While I constantly have to remind myself Lambda works with events, in this context I want to code against those messages as if they were incoming HTTP requests. I want to use Go’s standard library.
Perhaps the biggest helper for you is the example code. Just copy it and adjust to taste. You should be up and running with Cognito, API Gateway, and Lambda in no time.
Speed as in Performance?
Perhaps an important question for many. Just how long does it take for a response here? Is this feasible?
Yes! I believe so. Aegis will look up some information from Cognito using AWS SDK so your initial “cold” Lambda invocation will incur this penalty of waiting on a few requests. However, after that, everything necessary to verify the JWT is there. So subsequent “warm” invocations will be much faster.
I’ve seen response times vary in the range of 25ms to 2 seconds. Obviously the 2 second responses are those that need to deal with AWS SDK operations. The shorter responses are those that are just simply verifying a token and returning a simple API response.
Don’t worry, performance is acceptable.
Obviously, your mileage will vary here depending on what your event handlers are doing.
I think many people focus on the performance of AWS Lambda and API Gateway too much. I see it come up as the chief complaint all the time. That’s actually really good for Amazon if that’s all people can come up with.
Over the past year API Gateway has become a lot faster and now with native Go support, I’m seeing Lambda invocation times decreasing as well (for all languages actually). So folks…This is an addressable concern.
The reality is that yes, there is a small “tax” to pay for the convenience. Your typical Docker based API with Go will likely be faster. However, you’re going to need to load balance it in order to scale and you aren’t coding and deploying your Cognito protected API in a matter of minutes that way either.
Only you can determine if the development, deployment, and response speed is fast enough for your needs. For most general applications, I think it is.