Case study

CategoryArticles

Serverless Series: Implementing Secure Login with Amazon Cognito

In this article, you will learn:

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.

Introducing Amazon 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.

Cognito main

Why Amazon Cognito?

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.

Basic concepts and architecture of Cognito

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:

User Pool

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

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

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:

Cognito Lambda triggers

When creating a 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:

user pool review

  • the user uses their email as username
  • the user has the option to use MFA, but is not forced to do so
  • name, email and browser locale must be entered during registration
  • self-registration and password recovery are enabled for use for frontend integration through public API
  • notifications and verifications happen via email and the AWS SES service (recommended but requires additional configuration)
  • Configured one App client for frontend integration
  • basic token settings were left, but you can slightly increase the security of the user session, for example, by reducing the expiration of the refresh token
  • use tags as you can save info about the maintainer, or the environment and application according to which you can filter expenses in Cost Explorer

Identity Pool

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.

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:

Identity pool

  • a source of trust is used for the User Pool as described above
  • only logged-in users from the User Pool will get access to your services through an authenticated role
  • the authenticated role for now remains with predefined privileges, but in the case of using another AWS service, for example S3, it is necessary to set the user's access to a specific S3 bucket

An example of a secure login implementation with Cognito

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:

cognito architecture 3

Briefly in a few points that may not be visible on this part of the diagram:

  • the user logs in using the frontend web application
  • frontend login is implemented using Amplify library
  • the user pool is connected to the application database via Cognito triggers for reading and writing
  • during the authentication process login credentials are exchanged for JWT tokens
  • after login, the token obtained from Cognito is used for authorization when making API requests

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:

Cognito communication

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.

Cognito best practices and safety recommendations

Deploying and working with Cognito can be a bit tricky, especially for newbies, so here are some recommendations to help you avoid unnecessary problems:

  • Do not use one user pool for multiple use cases, or for multiple environments.
  • Use a separate App Client for each application type. In the case of a frontend application, you do not need to generate a client secret.
  • Set your session expiration correctly. Your session token should not be valid for more than one hour. You can do the same with the session refresh token, but set a slightly longer time, like a week or a month.
  • Store only non-sensitive user information in token claims that you can further verify in your application and use in an additional layer of security to access only certain records. It’s usually an identifier for the mapped user in the database, or the identifier of his organization.
  • Use the ALLOW_USER_SRP_AUTH (secure remote password protocol based authentication) method for client-side password encryption. If you are migrating your user accounts from an external source, ALLOW_USER_SRP_AUTH will not work for you and you will need to use ALLOW_USER_PASSWORD_AUTH which does not hash the password on the client side. This is because Cognito needs to get the unencrypted password so it can pass it to your lambda trigger to authenticate and migrate the external account.
  • Integrating external identity providers can be a bit confusing, as both User Pool and Identity Pool provide such an option. In most cases, it will be enough for you to use the integration via the User Pool, because the one in the Identity Pool serves to federate the identity and is additionally charged. You will know if you need this.
  • Think in advance about which attributes will be mandatory and what their settings will be. Attributes cannot be changed after the User Pool is created, and you could end up migrating the entire User Pool.
  • It’s also possible to set the guest role in the Identity pool, which serves to obtain temporary access for unauthenticated users. While this feature is useful in some cases, it will be better if you measure twice and cut once and properly check where you allow such a guest to access.
  • To deploy and manage your secure login solution, it’s more than appropriate to use a IaC tool such as CDK or Terraform. Your effort will be returned to you many times over in the future.

Cognito pricing

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:

  • $0.0055 per MAU above free tier limit
  • $0.015 for enterprise users who sign in through SAML or OIDC federation (above 50 MAU free tier limit)
  • $0.050 in addition per MAU for advanced security features like compromised credentials detection, adaptive authentication, advanced security metrics, and access token customization.

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.

Conclusion

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.

Similar blog posts

See all posts
CategoryCase Studies

Windy - The Extraordinary Tool for Weather Forecast Visualization

StormIT helps Windy optimize their Amazon CloudFront CDN costs to accommodate for the rapid growth.

Find out more
CategoryCase Studies

AWS Well-Architected Review Series: Healthcare Industry Client

Transforming healthcare AWS operations with StormIT using our expertise and the AWS Well-Architected Framework. Learn more.

Find out more
CategoryCase Studies

Srovnejto.cz - Breaking the Legacy Monolith into Serverless Microservices in AWS Cloud

The StormIT team helps Srovnejto.cz with the creation of the AWS Cloud infrastructure with serverless services.

Find out more
CategoryCase Studies

AWS Well-Architected Review Series: Renewable Energy Industry Client

See how StormIT optimized a renewable energy client's AWS infrastructure through the Well-Architected Framework. Explore now...

Find out more
CategoryCase Studies

Microsoft Windows in AWS - Enhancing Kemper Technology Client Solutions with StormIT

StormIT helped Kemper Technology Consulting enhance its technical capabilities in AWS.

Find out more
CategoryNews

Introducing FlashEdge: CDN from StormIT

Let’s look into some features of this new CDN created and recently launched by the StormIT team.

Find out more