This is the second post in a series of articles documenting my process creating a serverless multiplayer game - Mafia.
This article covers adding User Authentication and Authorization to the game.
Contents
- Setup AWS
- Infrastructure overview
- User authentication
- User authorization
- Quasar frontend
Setting up AWS for a new project
If you search online for AWS project best practices you will find a dozen different guidelines all espousing their method to be the "best". I'd like to avoid pushing my methodology on you here, but this is a quick rundown of the process that I used.
My main AWS account (the one that I first created) is an empty shell containing no resources. In this account I created an AWS Organization. Under an organization, you can create groups - I've made a group for Mafia. Inside the Mafia group I make two more groups; dev and prod. These two groups each have an AWS account in them.
In my root AWS account I then configured IAM Identity Center for Single-Sign-On (SSO) and created an SSO profile for myself with Admin Access to the two Mafia accounts. This new SSO account can now generate AWS CLI access keys for each of the deployments (prod/dev).
Infrastructure Overview
In this game users will have individual profiles. Logging in retrieves the associated profile from the database and tags all future API calls with the users' ID. The API will then use the provided ID to determine if the caller has permissions to perform whichever action they're attempting.
AWS has all the tools to create a robust serverless user authentication system and SST makes it super easy to get started. For this project I'm going to be using the following AWS services:
- AWS Cognito - to handle user authentication (validate passwords)
- AWS ApiGateway - to route frontend actions to backend functions
- AWS Lambda - the logic
- AWS DynamoDB - to store user profiles
There are two main 'routes' to the API for authentication; /noauth and /{other}. The /noauth route is exclusively used for signup/signin/recovery/etc actions (may be expanded in future).
The /{other} route is simple a placeholder for all other routes. Each of these calls are processed through a custom Lambda Authorizer which validates the users access token and then forwards to the AuthController Lambda.
DynamoDB setup
DynamoDB is a NoSQL database which offers huge horizontal scaling for a relatively low price point, following a pay-per-request model. There's plenty of ways to use DynamoDB but for this project I'm attempting to follow Single Table Design (STD??) principles as best I can.
The goal of STD is one big boy DynamoDB table that holds all entity data in one place. The benefits of this approach is speed (apparently?) and increasing the complexity of your business logic by a few magnitudes. I'll try until it gets to hard I guess.
With the single table approach it's important to have a good grasp on what the access patterns are going to be. That is, in what ways do I intend to query data. Theoretically, by knowing how you intend to query the data you can infer the optimal way to store it. If you are unfamiliar with DynamoDB this next part might be a bit confusing.
Currently my access patterns are as follows:
- [GET] Item by exact ID (PK + SK) - To retrieve a single record: User/Lobby/Game/etc
- [QUERY] Items by partition ID - To retrieve all records under a partition key: eg. A Lobby and all Users in it
- [QUERY] Items by type (GSI1) - To retrieve all records of a type Users/Lobby/Game
GET calls return exactly one record whereas QUERY's return all records that match.
In NoSQL Workbench I made a mock up of the data structure to visualize my GSI's.
Primary projection: The red marking shows that Lobby(PK=2143) contains records for itself (SK=A) and the user UncleGenghi (SK=U#1234). Note that I'm encoding the entity type into the SK.
GSI1 (itemsByType): Projects all records into their TYPE grouping.
User Authentication
As mentioned previously, I am using AWS Cognito to handle user registration and validation. The Cognito functions are exposed to the frontend via HTTPS API using ApiGateway. Calls such as /noauth/auth pass directly to the AuthController, in this example returning a JWT access token (providing supplied email and password are valid). The JWT token must then be used in the headers of all future requests.
User Authorization
I decided to use a custom Lambda Authorizer for this project with these two resources for reference: AWS Article, Code.
Custom Lambda Authorizers are great for adding fine grain authorization but come with some noticeable performance reductions and cost increases.
For starters, in terms of AWS resources a Lambda Authorizer is identical to a standard lambda function. This means that all API Gateway routes that flow through a Lambda Authorizer now count as 2 lambda invocations. Two invocations means an increase in time and cost. The more complex the authorizer, the slower every API route that uses it gets.
Below is an example of an API call that returns a list of all open lobbies. As we can see, before the GET /lobby/list call we must first be authorized.
Overall, while the authorizer definitely adds overhead to the API I believe that the ability to have granular access is crucial.
Quasar frontend
Quasar framework is my go-to when building complex web apps. It uses VueJS under the hood but comes with so many useful utilities that it's hard for me to justify not using it.
I think that both the best and worst thing about Quasar is how easy it is to create visually appealing and responsive frontends. I say "and worst" because to be honest, using Quasar feels like cheating. Prior to using Quasar I had very little frontend experience with vanilla Javascript and CSS, essentially zero foundational knowledge. Quasar lets me get away with not learning the basics and still succeed. Still not sure how I feel about that, but anyway...
This is my attempt at a login page. It's probably nothing fancy to any experienced WebDev's reading, but it's pretty up there for me. I linked it up to the backend and it works great. I may remove the self signup feature temporarily in place of an invite-only system so I can do some controlled public testing, but that's unimportant to this article.
To ensure that my design is appropriately responsive I'm using an app called ResponsivelyApp. The app lets me select several target screen sizes and validate my design on all of them in the same screen. Check it out.
That pretty much wraps up everything I wanted to cover in this article. In my estimation the main educational aspect of this article series is going to be the AWS design and integration. Frontend design/development is very much a necessary chore to me while API and cloud infrastructure design it where the real interesting stuff happens.
At the time of writing this I've made significant progress towards a lobby hosting/joining system. It's not quite at the stage where I can confidently write about it, but what I can say is that it leverages AWS IoT Core to facilitate real-time push notifications to the frontend.
If you're interested in this project I've made a discord (https://discord.gg/EcGx9h2fwy) where I post updates as I go. Happy to answer any questions whether technical or game related.
Cheers,
Top comments (1)
Nice article!