Freighter Dev Log 2: Securing the API

Written by stevenajohnson | Published 2023/10/30
Tech Story Tags: software-development | dev-log | startup | server-hosting | startup-lessons | dev-experiences | game-development | jwt

TLDRSince my last dev log, I’ve made good progress towards porting Freighter over to AWS. This dev log will be focused on how I set up authorization for my API Gateway. I decided to build my own Lambda Authorizer so that I could have more control over the authorization flow. For the authorizer, I chose to go with JWT.via the TL;DR App

Since my last dev log, I’ve made good progress toward porting Freighter over to AWS. For those who have not read the first dev log, Freighter is a game hosting service with a usage-based payment plan.

If you’re interested in reading more about what Freighter is and its purpose, you can read about it here.

This dev log will be focused on how I set up authorization for my API Gateway. I decided to build my own Lambda Authorizer so that I could have more control over the authorization flow. For the authorizer, I chose to go with JWT (Json Web Token)/Refresh Token authorization.

The reason that I chose to go with a JWT approach instead of using Cognito is because I get a little more control and can really get into the weeds. This gives me more experience with security as well as the ability to customize as I see fit.

Concerns With JWT Security

I have seen others express concern about the security of a JWT. Most of the concern originates from the fact that you cannot revoke access to the API until the JWT expires. That is why in my authorizer, I made the JWT have a very short TTL (time to live) and made the refresh token long living. The JWT is only viable for two minutes before it expires.

The refresh token will remain valid until it is used to generate a new JWT, at which point a new refresh token will be generated. With a service like Freighter, users are going to get most of their work done within two minutes.

Most of the user interaction is simply turning the game server on and off as well as updating the server settings. This can be done quickly and will most likely be completed within one JWT’s TTL. For convenience, the refresh token is stored as a secure http-only cookie.

When a request is made to the API, but the JWT is expired or invalid, the API will return a 403 (Forbidden) response which the front-end should capture and automatically attempt to refresh the JWT.

Seeing the refresh token is long-lived, this can be done automatically the next time the user visits the site, and it’s as if their session never ended. Because I built the authorizer, I can change the JWT or the refresh token TTL by simply updating the Lambda function that handles authorization.

Designing the Database to Support JWTs

The general flow for authorization starts with the database. For the database, I’m using DynamoDB with a single-table design. Because I’m using a single-table design, I have to first decide the access patterns to use for the different types of data stored. Currently, the user information requires a single index, which uses their username for the partition and sort key.

This means that usernames across the site have to be unique. This will make it easier down the line to add friends and look up user information. This also means later on I could add Global Secondary Indexes (GSIs) which allow me to query the same information from DynamoDB using different data.

For instance, maybe I want to give the user the opportunity to sign in using their phone number. I could create a GSI that has the same access pattern as the primary key but instead uses their phone number instead of their username.

This GSI could also be used for adding friends who have verified their phone numbers.

Designing the JWT

Now that the user access pattern is defined, I can move on to building the Lambda Authorizer. The JWT is made up of just a few fields:

  • ‘iss’: issuer (the freighter auth API)

  • ‘sub’: subject (username)

  • ‘aud’: audience (the freighter data API)

  • ‘exp’: expiration timestamp (equal to two minutes)

  • ‘nbf’: not before (time that it was issued, stops the use of JWT before the current timestamp)

The authorizer simply has to verify that the JWT is valid and return an IAM policy. If the JWT is invalid due to expiration, invalid issuer, audience, or the signature doesn’t match, you set the Effect field to ‘deny’ in the IAM Policy, and the API will return a 403 status code.

If the JWT is valid and passes all the checks, it will set the Effect field to ‘allow’ which will allow the request through to the API gateway.

Some of the Lambda functions that handle API requests also use the JWT to collect information from the database. For example, the endpoint “/gameservers” will only return the game servers that belong to the person requesting.

That is because the Lambda function will take the subject (‘sub’, username of the requester) field from the JWT and use that as the primary key when querying. This makes the URLs shorter and more concise.

This idea of collecting information from the JWT is one of the main reasons I chose to go with this form of authorization. In the future, I could change the JWT to hold the permissions that the user has.

Lambda functions can use these permissions to check access to the resources without having to hit the database. This saves cost because it requires fewer database calls as well as execution time to verify access.

JWT Signing and Invalidating

There is one small part of the authorization left. That part is the key that is used when signing and verifying JWTs. This key lives in the AWS Secret Manager. If somehow somebody guessed or cracked the JWT signature key, they would be able to generate their own tokens without the refresh token and impersonate someone on the service.

To counteract this, I stored the signature key in AWS Secret Manager. At any point, I can change the value within this manager, and all JWTs will be invalid. Seeing JWTs rely on signing keys to issue, as well as to verify, if I change the signature key all verifications will fail.

The authorizer will return 403 status codes, and anyone with a valid refresh token will be able to generate a new JWT with the new signature. I could also set up a CloudWatch event to periodically change this signature to further increase security.

What Is Next?

Now that the authorization is complete, I can focus on porting endpoints that I created when using PocketBase to AWS. This is the last stretch of building out the minimum viable product.

After that, the next steps would be to start working on the front-end of the site and switching the HTTP request to the new endpoints. I expect this stage of the project will take some time to ensure that the switch is bug-free.

The next milestone will be when all of the endpoints have been ported over, but the next dev log will probably come out once the front-end has been completed. Thanks for reading, and I’ll see you in the next blog post.


Written by stevenajohnson | I am always interested in learning new things and hope to create content that can help others.
Published by HackerNoon on 2023/10/30