Frictionless Serverless Development: Part 3— Authentication
Best practices for implementing authentication in serverless applications, focusing on how to achieve frictionless user sign-in and access control without compromising security or scalability.
Welcome to Part 3 of the Frictionless Serverless Development series. In Part 1, we described setting up your environment to ensure that we had code consistency. Then with Part 2, we talked about configuration, express and github actions.
With this part, we’re going to discuss setting up AWS Cognito for Authentication which is one of the main areas that all applications will likely need. I was going to go through more of the Authorization aspect but I believe we’ll save that for another part in this series as Authentication is likely a large enough chunk to go over right now so I apologize for the change up at this point in the series.
Overview
This article will be targeting AWS Cognito and Express middleware to ensure that are are authorizing our specific routes. We could have gone the route of writing our own authentication layer, however, when we can use AWS Cognito to handle many of the complex tasks involved with authentication and authorization as well as implementing 2FA amongst others. I feel it is best to save our time and utilize a service that specializes in those capabilities. As with my other articles, here is a brief overview of the technology that will be involved:
AWS Cognito
Amazon Cognito lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily. Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Apple, Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0 and OpenID Connect.
Express Middleware
Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle.
Middleware functions can perform the following tasks:
Execute any code.
Make changes to the request and the response objects.
End the request-response cycle.
Call the next middleware function in the stack.
Getting Started
If you have followed along so far, we’ll be editing the serverless.yml
file to start with in order to setup Cognito. In this case, we’re using the Resources section of the yml file thus using CloudFormation templates to configure AWS Cognito. This will setup our User Pool and allow us to start adding authentication to our application.
When we last ended our prior article, our serverless.yml
file looked like this:
Setting up the Cognito User Pool
We need to add into that to provide configuration for our user pool via a resources:
section. To start, we need to add in a UserPool to add in all of our properties that we want to maintain on the user record itself within Cognito; we will keep this skinny for now as later on we will actually be managing our user within a database for profile based attributes. If you’re looking for social authentication, that will be coming in a much later part of the series, for now we’re going to be targeting the common username and password approach.
Most of the configuration above is fairly self-explanatory. However, let’s dig in a little deeper on what is going on here since if you’ve never used CloudFormation this syntax is a bit odd.
We are defining a “Resource” with a “Type” that explains to CloudFormation what it is we are attempting to create. CloudFormation contains several different properties for each of these types that allows us to configure it to our liking. You’ll likely want to refer to the AWS::Cognito::UserPool documentation for customizing your UserPool.
For the UserPoolName, we are leveraging the name of the service as well as adding in the current stage; this allows us to create a different user pool for each individual stage much like you have a dev, stage and prod site this is what allows us to enable that behavior without sharing the user pool across to different environments.
UsernameAttributes tells Cognito what we deem as our identifier for the user which allows for phone_number or email. IMPORTANT: UsernameConfiguration has an attribute called CaseSensitivity you will 99.99% want this to be false. It cannot be changed once the UserPool has been deployed without forcibly re-creating the UserPool.
AutoVerifiedAttributes tells Cognito if you would like it to automatically attempt to verify the email (or phone number) by sending a message and having the user either click a link or receive a code to put in. We generally consider verifying emails and phone numbers as best practice.
Next, you have Policies. Currently this only maintains a single area for PasswordPolicies. Here you can configure the minimum strength of the password that is being used to create the account.
Last but not least in our configuration, we define out the user schema. Our user schema in this particular case we have a given name (first name), family name (last name) and an email address. We could add in all sorts of other attributes for the user, however, we tend to keep that logic within our application. It is nice to have the name in Cognito for when you have to go and find them manually in the AWS Console when an issue or otherwise arrises.
Setup our User Pool Client
Having a User Pool is great, however, we will be unable to use our User Pool unless we configure a User Pool Client to leverage with it. We’ll append our Resources section to add in a UserPoolClient that will allow us to authenticate given that particular client as well as validate that the authentication came from that “audience” or User Pool Client.
Again, a lot going on here, in this case we’re specifically stating that the client has explicit authentication flows which allows for secure remote password (i.e. never is passed to your backend server) and is also allowed to refresh the token to exchange it for a new token.
We also follow specific OAuth flows, implicit being the main flow with a localhost callback and scopes of email, openid and profile.
The last tidbit here is the SupportedIdentityProviders. For now, this is just COGNITO but in a future article this section can expand to support Federated Login providers such as Apple, Google, Facebook, OpenID, SAML, etc.
Configuring our Environmental Variables
Now that we have a UserPool and UserPoolClient configured, we need to be able to get certain environmental variables back into our serverless functions so that we can actually check our authentication through a middleware. In our provider
section, we will add in a child for environment
. Here, we need a few items:
What we are doing here is ensuring that we can access the region, user pool id and user pool client id within our application from using environment variables. Without this, we would be unable to use cognito within our express middleware to validate a login.
Pat yourself on the back, we’ve completed configuring our yml file and that’s enough CloudFormation for now… yes, there will be more later.
Our full serverless.yml
file at this point:
Express Authentication
We’re not going to start looking at how to add in authentication into our express routes. Currently we have a single route from our prior area, but we’ll need to create a middleware and we’ll create a protected route that will validate our authentication.
We are going to utilize a third party package aws-cognito-express. This package allows us to validate our token against AWS Cognito.
Next, we need to create a middleware folder and create our middleware file: api/middleware/auth.js
For now, our middleware file will become nice and easy, we’re creating this as a custom middleware because in a future part we will be extending this.
The process.env
here is what we configured for our environment from our serverless.yml
environment setup. Now to handle our authentication errors, we’ll want to modify our api/app.js
file to ensure that we’re catching our errors properly.
Now, let’s add a new route to our index router to simply return back the parsed JWT from Cognito with our authentication middleware:
api/routes/index.js
Push and Deploy
Using our earlier Github rules, it might be time to commit and push so that we can get our URL to validate that everything is pushed up. Grab the deployed url from the github action build logs:
Now, navigate to: https://xxxxxxx.execute-api.us-east-1.amazonaws.com/me replacing the xxxxxxx
with your specific endpoint. You should see an authorization error stating:
If you do, great! This means that we’ve successfully added the authentication middleware and to add authentication to any endpoint we just need to add the auth
middleware to our express routes.
Testing our Authentication
Since we know that it already declines our authentication, we likely want to test using our authentication, we’re going to need to leverage the AWS CLI. This way, we can essentially leverage our user pool client without having to configure anything else (or write any front-end application yet). This does assume that you’ve followed part 1 and 2 and have AWS setup along with any profiles that you are going to be using.
First, we’re going to need to find our user pool, note that --max-results
is required, so I’ve set it to 10 in this example just in the event that you have other cognito pools currently setup.
Our output will look like the following, make note of the Id value in your output:
We also will need to know what our User Pool Client is, this is what allows us to talk to the user pool, replace the us-east-1_XXXXXXXXX
with your particular user pool.
We need to make note of the ClientId in the output:
Create a user and set the password (we really don’t care about the output in these cases since we’re just attempting to get the user setup so we can validate a token).
Now, we’re going to need to login. However, we need to be able to generate out the SRP portions of the request which will require some additional help. First let’s add a new package to our development dependencies:
Secondly, we want to create a helper script to be able to generate our token in a CLI, create a new file: bin/auth.js
with the following contents:
We can now call this with node
to get our token to submit:
This will respond back with a token if your authentication was successful, example. If you would like to see what the JWT token contains especially since we’re using the ID token in this case, head on over to jwt.io and paste it in!
Now, let’s call our protected route inside of postman, setting the Authentication to Bearer and pasting in the response from the above call. You should now see a response similar to the following:
Success! You now have authentication!
Next Steps
Following this article, we’ll be bringing in Prisma, our ORM of choice for node.js projects.
Related posts
The best ideas don't wait. Let's talk & make it happen.
© Spark Labs 2025. All rights reserved.