/terraform-aws-cognito-auth

Serverless Authentication as a Service (AaaS) provider built on top of AWS Cognito

Primary LanguageTypeScriptMIT LicenseMIT

Github Action Codecov Gitter GitHub dependabot

Terraform AWS Cognito Auth

Add authentication to your Single Page Application (SPA) within minutes and take full control of the authentication flow including customizable email templates and a beautiful default UI. See the live demo.

A Terraform module to setup a serverless and easily customizable Authentication as a Service (AaaS) provider in front of API Gateway using AWS Cognito User Pools.

Features

  • Authentication using email and password or refresh token
  • Registration, password reset and verification
  • Completely customizable transactional emails
  • Optional multi-part default email templates (see screenshots)
  • Optional beautiful and mobile-friendly default UI (see screenshots)
  • Federated identities using Cognito Identity Pools and User Pools
  • A+ security rating on Mozilla Observatory (CSP, HSTS, etc.)
  • Excessively tested with automated unit and acceptance tests
  • Serverless, extremely scalable and cost effective
  • However, there are some limitations

Architecture

Architecture

This module creates a REST API using AWS API Gateway, Lambda and Cognito User Pools to enable registration, authentication and account recovery without the necessity for the implementation of complex OAuth authentication flows. It was originally inspired by LambdAuth but uses User Pools in favor of Identity Pools because exposing (even temporary) AWS credentials is a security threat.

Account registration and recovery circumvent Cognito's default verification logic and emit verification codes to an SNS topic which can be hooked up to a Lambda function handling delivery via SES using default multi-part email templates. This behavior is optional and can be customized by implementing a custom Lambda function handling email delivery. Furthermore, a beautiful and mobile-friendly default UI can be deployed to a custom subdomain within your hosted zone.

Cost

AWS Cognito is free for up to 50.000 monthly active users. After that, pricing starts at $ 0,0055 per monthly active user. Additional cost will be attributed to AWS Lambda, API Gateway and CloudFront but it should be very reasonable compared to what AaaS providers like Auth0 charge. While this module does not provide all features offered by other providers, it should be quite sufficient for securing a Single Page Application.

Usage

Add the following module to your Terraform configuration and apply it:

module "cognito-auth" {
  source  = "squidfunk/cognito-auth/aws"
  version = "0.4.2"

  namespace                      = "<namespace>"
  region                         = "<region>"
  cognito_identity_pool_name     = "<pool-name>"
  cognito_identity_pool_provider = "<pool-provider>"

  # Optional: Default UI
  app_hosted_zone_id             = "<hosted-zone-id>"
  app_certificate_arn            = "<certificate-arn>"
  app_domain                     = "<domain>"
  app_origin                     = "<origin-domain>"

  # Optional: Email delivery
  ses_sender_address             = "<email>"
}

All resources are prefixed with the value specified as namespace. If the S3 bucket name (see below) is not explicitly set, it's set to the given namespace which means there must not already exist an S3 bucket with the same name. This is a common source of error.

The cognito_identity_pool_provider should match the domain name under which the authentication provider should be deployed, i.e. it should be equal to app_domain. Also note that SES is sandboxed by default, so every email address needs to be verified for delivery. Contact AWS to exit sandboxed mode for production use.

Also see the example configuration and the live demo.

Configuration

The following variables can be configured:

Required

namespace

  • Description: AWS resource namespace/prefix (lowercase alphanumeric)
  • Default: none

region

  • Description: AWS region
  • Default: none

api_stage

  • Description: API deployment stage
  • Default: "production"

cognito_identity_pool_name

  • Description: Cognito identity pool name
  • Default: none

cognito_identity_pool_provider

  • Description: Cognito identity pool provider
  • Default: none

Optional

Default UI

app_hosted_zone_id
  • Description: Application hosted zone identifier
  • Implies: app_certificate_arn, app_domain and app_origin
  • Default: ""
app_certificate_arn
  • Description: Application domain certificate ARN
  • Implies: app_hosted_zone_id, app_domain and app_origin
  • Default: ""
app_domain
  • Description: Application domain
  • Implies: app_hosted_zone_id, app_certificate_arn and app_origin
  • Default: ""
app_origin
  • Description: Application origin domain (target domain)
  • Implies: app_hosted_zone_id, app_certificate_arn and app_domain
  • Default: ""
bucket
  • Description: S3 bucket name to store static files
  • Default: "${var.namespace}" (equal to namespace)

Email delivery

ses_sender_address
  • Description: SES sender email address
  • Default: ""

Example

Let's say we want to secure an application hosted on admin.example.com using the default UI. First, add the following lines to your Terraform configuration and apply it:

module "cognito-auth" {
  source  = "squidfunk/cognito-auth/aws"
  version = "0.4.2"

  namespace                      = "example-auth"
  region                         = "us-east-1"
  cognito_identity_pool_name     = "Example Auth"
  cognito_identity_pool_provider = "login.example.com"

  # Optional: Default UI
  app_hosted_zone_id             = "Z*************"
  app_certificate_arn            = "arn:aws:acm:us-east-1:..."
  app_domain                     = "login.example.com"
  app_origin                     = "admin.example.com"

  # Optional: Email delivery
  ses_sender_address             = "accounts@example.com"
}

Now, when the user visits admin.example.com/dashboard, the initial API request should detect a 401 Unauthorized response for an invalid or expired identity token and redirect to the default UI:

https://login.example.com/?redirect=dashboard

After successful authentication, the default UI will redirect to the URL specified in app_origin appending the path part specified in the redirect parameter and the identity token in an URI fragment:

https://admin.example.com/dashboard#token=<token>&expires=<timestamp>

Then, after parsing the URI fragment and extracting the token, the application can repeat the request including the identity token as an authorization header:

Authorization: Bearer <token>

If the user checks the Remember me checkbox during the authentication process, a refresh token which is valid for 30 days is issued and sent to the client as a secure HTTP-only cookie. When the access token expires after 1 hour, the client is again redirected to the default UI which will immediately perform a password-less authentication using the refresh token.

To sign out, the application must redirect the user to the following URL:

https://login.example.com/leave

This will invalidate all tokens including the refresh token stored in the secure HTTP-only cookie.

Demo

Live demo

A live demo of this project can be found here. Please note that emails are configured to remain undelivered due to security reasons. If you want to try authentication, you may use the following credentials:

Username: jane.doe@example.com
Password: depl0y&D3STROY

The target domain is localhost:8000, so you can start a local development server on that port in order to receive the identity token after a successful authentication attempt.

Screenshots

Default UI

Emails

Limitations

By default, AWS Cognito does only allow minor customizations of the whole authentication flow - specifically multi-part emails are not supported; welcome to the 21st century. To work around these restrictions registration and password reset were decoupled using the Cognito Identity Service Provider admin APIs. Verification is implemented with custom verification codes and email delivery. However, as Cognito currently does not support setting the password for a user through the admin API, the user is deleted and recreated with the exact same identifier (a UUID). This is heavily tested with acceptance tests and just works, but it's not ideal. Hopefully AWS will address these issues in the future.

License

MIT License

Copyright (c) 2018-2019 Martin Donath

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.