Cyber security and especially the protection of user identity is one of the main components of well-architected applications. Ensuring all these security requirements can be not only time-consuming for the development team, but also cost-inefficient from a business point of view.
In this article, we'll share my experience of securing applications in a serverless environment with an Amazon service called Cognito.
Amazon Cognito is an identity and access management service from Amazon Web Services that enables developers to easily implement user sign-in, sign-up, and access control for their web and mobile applications.
**It offers a secure and scalable way to manage user identities, including social identities (such as Facebook, Google and Amazon) as well as enterprise identities via SAML 2.0 **.
With features like advanced security, a scalable user directory, and federated sign-in capabilities, it supports over 100 billion authentications per month. Cognito is developer-centric, customizable, and integrates with a broad range of AWS services, supporting compliance regulations and the latest security standards.

Amazon Cognito offers a number of features and benefits that make it an attractive choice for developers and enterprises looking to implement secure and efficient identity and access management in their applications. As you will see below, it is also an ideal companion for serverless application development. Here's an overview of some of the key features and benefits:
Easy integration and development: Enables fast and easy integration of login and authentication mechanisms into your applications without the need for complex coding or infrastructure management. For seamless integration, AWS provides a frontend library called Amplify, which you can use to integrate Cognito security elements into your frontend application. Cognito also provides OpenID Connect endpoints so you can use your familiar authentication libraries with OIDC support.
Seamless Integration with Serverless Services: Provides smooth integration with other AWS serverless services such as AWS Lambda, AWS AppSync, Amazon API Gateway, and Amazon DynamoDB. This integration allows developers to create fully serverless applications with secure authentication and authorization processes without managing any server infrastructure. When developing a serverless application, this is the first thing we integrate into the application as it works like an excellent companion who knows how to keep a secret.
Access control and management: Allows developers to define access and permission rules for users or groups of users, ensuring that users only have access to the resources they need. An excellent demonstration of the deep integration of Cognito's security elements is the integration into the AWS service called AppSync, which serves to create API interfaces in a serverless environment. Thanks to the strongly typed GraphQL< schema, you can secure your API calls down to the field level using GraphQL directives. But that is a topic for another article.
Built-in protection mechanisms: You can add an additional layer of security for your customers by enabling MFA (Multi-factor Authentication). Cognito can also detect and prevent, in real time, the reuse of compromised credentials. What's more, with adaptive risk-based authentication, it detects unusual login activity, such as attempts from new locations and devices, assigns a risk score to the activity, and lets you choose whether to prompt users for additional verification or block the login request. Let's be honest, who wants to spend time implementing these features when we can focus on the features that sell our product? And we don't mean only this case.
User migration from external sources: In case you already have your user base and want to start using a secure solution from Cognito, the service gives you a few options. You can easily import them via CSV, use the API, or just-in-time (JIT) migration process via trigger. JIT migration allows users to seamlessly transition from the old authentication system to Cognito without having to go through a separate migration process. This ensures a smooth and uninterrupted user experience and has proven to be the easiest way to give your existing customers access to your new application. You simply write a piece of code that validates and retrieves user identity with your original identity source and deploy it as a trigger in Cognito. This trigger runs as a serverless script of the AWS Lambda service, which is integrated into Cognito, so the service itself takes care of the rest.
Easy integration with external identities: This feature allows users to log in through popular identity providers such as Google, Facebook, Apple and Amazon. This list includes the most popular identity providers, but with a little knowledge of OIDC and OAuth 2.0 protocol, you can easily integrate other providers (Twitter, PayPal, Salesforce and so on). And there's also support for enterprise customers via SAML 2.0.
Scalability: As a managed cloud service, Amazon Cognito is designed to automatically scale and handle millions of users worldwide, ensuring high availability and performance even for very large applications. This is another thing that makes it an ideal companion for developing serverless applications. But it does have its service quotas. We can divide them into two types, soft and hard. In the case of soft limits, you can request a quota increase from support. Unfortunately, this is not possible with the hard limit.
Cost-effectiveness: Offers a flexible pricing model based on the number of active users, allowing businesses to optimize costs in line with their needs and growth. What's more, you pay nothing for the first 50,000 users. For the record, an active user is a user who logs in at least once in a given month. We will talk more about the price model at the end of the article.
The Cognito service has many functions and security elements, the description of which can be quite lengthy. Therefore, let's talk about the most important ones that developers encounter most often:

With classic session authentication using username and password, user data are stored along with passwords in your application together with other application data. In the context of security, this can be a bottleneck and can also be a security risk because your solution is only as secure as you can think to make it, especially in the case of saving passwords. We hear daily about the leakage of user accounts from some well-secured online services. User Pool is primarily your user account database managed by Cognito. It allows developers to quickly add login and registration functions to their applications without having to build and maintain their own user management system. It adds other features that simplify the development of the application, such as identity management, 2-factor authentication, user roles, sending emails, SMS and more.
App client is like an access point to your user database and thus enables your application to communicate with the User Pool. When you create an App Client, you will receive a special ID and, depending on the settings, secret codes that you will use in your application. It’s especially useful to set up a separate client for each type of application. For example, frontend applications usually do not need secrets.
Lambda triggers are integration helpers that automatically perform various tasks or actions based on what is happening with your user data. Basically, they give you the flexibility and power to do almost anything you want, automatically, in response to events happening in your User Pool. You program it by yourself in one of the supported languages. These triggers are divided into four categories that speak for themselves:

When creating a Cognito User Pool, you have to go through a fairly decent list of settings, which would take quite a bit of time to go through one by one. Also, different use cases sometimes will need different settings. Therefore, we have prepared an overview of the User Pool settings for use in a regular application:

Unlike the User Pool, the Identity Pool serves as a key that allows your application to access various AWS services on behalf of your users. Think of it as an entry ticket to a large amusement park (AWS services) where each attraction (a service like S3 for file storage, etc.) requires you to show a ticket. When a user signs in through the Cognito User Pool or another identity provider (like Facebook, Google, etc.), the Identity Pool assigns them temporary AWS credentials. These credentials are like the entry ticket that authorizes them to use certain AWS services according to the rules you set. For example, you can allow users to store files in a specific S3 bucket.
Cognito Identity pool has slightly less visible settings and further configuration takes place at the level of setting access rights for assigned roles. This already requires more extensive knowledge with identity and access management (IAM) in the AWS cloud. So the basic settings will include:

In the previous section, we covered the main elements of internal architecture and showed what the basic setup in the web console may look like. Now we can look at how the integration will look like in the web application. In the diagram from the introduction to serverless, one part concerns the use of Cognito for secure login. Usage in a serverless web application might look similar to this diagram:

Briefly in a few points that may not be visible on this part of the diagram:
Regarding login and subsequent authorization, Cognito draws on the OpenID Connect (OIDC) standard. This method ensures a secure authentication process using current security standards. Let's look at the simplified view of the whole process that AWS provides us on its website:

From the diagram you can see that the whole process is quite complex, and therefore integration into your application may not be easy. To simplify the integration of AWS cloud services into your frontend or mobile applications, AWS provides the Amplify library. The entire Amplify library is extensive. For Cognito integration we will only need the @aws-amplify/auth package from npm repository. First, you will need to configure the library for use with your User and Identity pool:
import { Auth } from '@aws-amplify/auth'
const config = {
Auth: {
region: 'us-east-1',
userPoolId: 'us-east-1_XXXXXXXXX',
userPoolWebClientId:'XXXXXXXXXXXXXXXXXXXXXXXXXX',
identityPoolId:'us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
},
}
Auth.configure(config.Auth)
You can then use other functions of the Auth namespace to login and register. Here are a few uses that should make this clear:
import { Auth } from '@aws-amplify/auth'
const signUp = async (username, password, attributes) => {
const { user } = await Auth.signUp({
username,
password,
attributes,
autoSignIn: {
enabled: true,
}
})
return user
}
const confirmSignUp = async (username, confirmationCode) => {
try {
await Auth.confirmSignUp(username, confirmationCode)
return true
} catch {
return false
}
}
const resetPassword = async (username, resetCode, newPassword) => {
const data = await Auth.forgotPasswordSubmit(username, resetCode, newPassword)
return data === 'SUCCESS'
}
const signIn = async (username, password) => {
const user = await Auth.signIn(username, password)
return user
}
const isSignedIn = async () => {
try {
const session = await Auth.currentSession()
return session.isValid()
} catch (err) {
return false
}
}
// the token will be used in the authorization header during the API request
const userToken = async () => {
const session = await Auth.currentSession()
return session.getIdToken().getJwtToken()
}
The last function demonstrates how you can get a JWT token for authorization against your backend. Use this token in the Authorization header when calling your API. The library will take care of the rest.
If you need to retrieve data from your application into the token, such as the customer or company id, you need at least one more trigger. You can then retrieve this data in your application and thus verify your identity or access data in your database more easily. I will show an example of the PreTokenGeneration lambda trigger, which reads the user from the database and inserts their IDs into the claims of the issued JWT token.
import { PreTokenGenerationTriggerHandler } from 'aws-lambda'
import { findUserByEmail } from './database/user'
export const handler = async e => {
const { email } = e.request.userAttributes
// your implementation of fetching the user from the database
const user = await findUserByEmail(e.request.userAttributes.email)
// If you don't have the user in the database, you can either add it here or use another trigger
e.response = {
claimsOverrideDetails: {
claimsToAddOrOverride: {
// These values will be available in your serverless application in e.identity.claims
userId: user.id,
tenantId: user.tenantId,
},
},
groupOverrideDetails = {
// You can also set up roles directly from your app
groupsToOverride: [...user.role],
},
}
return e
}
The whole implementation can be a bit more complex, especially when it comes to setting permissions, but these few examples should give you an overview of the implementation into a web application.
Deploying and working with Cognito can be a bit tricky, especially for newbies, so here are some recommendations to help you avoid unnecessary problems:
Cognito pricing is structured around two main components: User Pools and Identity Pools.
For User Pools, you pay based on monthly active users (MAUs), with the first 50,000 MAUs being free. Above that, there's a tiered pricing model.
For Identity Pools, costs are associated with the AWS resources you use (like AppSync, API Gateway, S3.) through federated identities, without direct charges for the number of identities.
Additionally, there are charges for optional features like SMS messages for phone verification.
The pay-as-you-go pricing model ensures that organizations can scale their authentication mechanisms as their needs evolve, without being burdened by upfront costs or wasted resources. Among the prices that might interest you are:
Since we probably won't be updating it here regularly with changes in pricing policy, you can find the most accurate and up-to-date pricing on the official AWS Cognito pricing page.
In conclusion, Amazon Cognito stands out as a complete solution for implementing secure login in serverless environments. It offers a blend of security, scalability, and cost-efficiency that is hard to match with traditional or self-hosted authentication systems. We have experience with deployment and management of self-hosted OAuth 2.0 and OpenID server solutions and we hope we won't have to go back to them. By using services like Cognito, we as developers can ensure that our applications are not only protected against common security threats, but also able to adapt to the growing demands of our application user base without significant cost or operational overhead. And this applies not only from the point of view of the price, but also of the effort and time spent.
With extensive experience as a developer, Roman specializes in serverless technologies and containers. His AWS expertise extends to backend and frontend development, where he excels in designing and implementing scalable solutions that leverage cloud services efficiently. Roman also demonstrates a deep understanding of infrastructure as code (IaC), employing tools and practices to automate and manage infrastructure effectively.