Cognito Hosted UI with CloudFormation

Fabio Gollinucci
5 min readDec 19, 2023

Almost every project needs authentication at some point. This is how to describe configuring Cognito via CloudFormation to speed up the user integration part of your application.

UserPool

The user pool is the main resource that store users registered or signed in using social providers.

UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub "${Project}-${Environment}"
DeletionProtection: ACTIVE
AutoVerifiedAttributes:
- "email"
UsernameAttributes:
- "email"
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
MfaConfiguration: "OFF"
Policies:
PasswordPolicy:
RequireLowercase: true
RequireSymbols: false
RequireNumbers: true
MinimumLength: "8"
RequireUppercase: true
AccountRecoverySetting:
RecoveryMechanisms:
- Name: "verified_email"
Priority: 1

This configurations use the “email” field as main user identifier (UsernameAttributes), tell Cognito to verify it sending a verification code (AutoVerifiedAttributes). The email, once verified, is also used to recover password (AccountRecoverySetting).

The attributes schema just contains the email attributes, other standard attributes like family_name, gender, given_name, locale, middle_name, name and so on can be directly used without configurations. Custom attributes ca be declare as used with “custom:” prefix like custom:my_custom_attribute.

Remember to activate the DeletionProtection in order to prevent accidentally replacement and consequentially lost of all users.

User Pool Client

The user pool client describe a client application that will connect to Cognito to perform login, registration and any other user-related actions.

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: [GoogleIdentityProvider, FacebookIdentityProvider, AmazonIdentityProvider]
Properties:
ClientName: !Sub "${Project}-${Environment}"
UserPoolId: !Ref UserPool
SupportedIdentityProviders:
- COGNITO
- Facebook
- Google
- SignInWithApple
GenerateSecret: false # enabling this will break the frontend login
AllowedOAuthFlowsUserPoolClient: true
CallbackURLs: !Ref CallbackURLs
LogoutURLs: !Ref CallbackURLs
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- email
- openid
- profile
- aws.cognito.signin.user.admin
TokenValidityUnits:
IdToken: days
AccessToken: days
RefreshToken: days
IdTokenValidity: 1 # from five minutes to one day
AccessTokenValidity: 1 # from five minutes to one day
RefreshTokenValidity: 30 # from 60 minutes to 10 years.

There are different type of login and registration experiences that can be handled with Cognito:

  • AWS managed hosted UI for login, registration and handle OAuth2 from your application
  • Self hosted frontend for login, registration and directly connect your application to Cognito

This configuration is handled by AllowedOAuthFlowsUserPoolClient properties.

If is is set to true you will also need to configure:

  • CallBackURLs: Your frontend login landing page URL.
  • LogoutURLs: Your frontend logout landing page URL.
  • AllowedOAuthScopes: The OAuth 2.0 scopes that ca be used.
  • AllowedOAuthFlows: Add support for authorization code, implicit, and client credentials OAuth 2.0 grants.

This will enable the AWS Hosted UI and allow user to login and register from it.

If is is set to false you will need to handle login, registration, confirmation and password reset using the Cognito APIs.

The identity pool

The identity pool handle user’s access to AWS resources like DynamoDB or S3 directly using AWS API with its own authentication system (IAM).

IdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
IdentityPoolName: !Sub "${Project}${Environment}Identity"
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId: !Ref UserPoolClient
ProviderName: !GetAtt UserPool.ProviderName

Describe the role that authenticated users can assume in order to call AWS APIs and access protected resources.

CognitoAuthorizedRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${Project}-${Environment}-cognito-authorized"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Federated: "cognito-identity.amazonaws.com"
Action:
- "sts:AssumeRoleWithWebIdentity"
Condition:
"StringEquals":
"cognito-identity.amazonaws.com:aud": !Ref IdentityPool
"ForAnyValue:StringLike":
"cognito-identity.amazonaws.com:amr": authenticated
Policies:
- PolicyName: "CognitoAuthorizedPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "cognito-sync:*"
- "cognito-identity:*"
Resource: "*"

Attach the role to the identity pool.

IdentityPoolRoleMapping:
Type: AWS::Cognito::IdentityPoolRoleAttachment
Properties:
IdentityPoolId: !Ref IdentityPool
Roles:
authenticated: !GetAtt CognitoAuthorizedRole.Arn

Cognito can be used to allow unauthenticated user to also assume a role, this is used for example for tracking with ClooudWatch RUM, do not made public an access to S3 bucket or a DynamoDB table.

Social Identity providers

A classic authentication use case includes access via social media, this can be managed by configuring the providers made available by AWS.

AmazonIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: "LoginWithAmazon"
ProviderDetails:
client_id: "xxxxxxxxxxxxxxxxxxxx"
client_secret: "xxxxxxxxxxxxxxxxxxxx"
authorize_scopes: "profile postal_code"
ProviderType: "LoginWithAmazon"
AttributeMapping:
email: "email"

GoogleIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: "Google"
ProviderDetails:
client_id: "xxxxxxxxxxxxxxxxxxxx"
client_secret: "xxxxxxxxxxxxxxxxxxxx"
authorize_scopes: "profile email openid"
ProviderType: "Google"
AttributeMapping:
email: "email"

FacebookIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: "Facebook"
ProviderDetails:
client_id: "xxxxxxxxxxxxxxxxxxxx"
client_secret: "xxxxxxxxxxxxxxxxxxxx"
authorize_scopes: "public_profile,email"
ProviderType: "Facebook"
AttributeMapping:
email: "email"

AppleIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: "SignInWithApple"
ProviderDetails:
client_id: "xx.xxxxx.xx" # the service bundle id
team_id: "xxxxxxxxx"
key_id: "xxxxxxxxxx"
private_key: "--- xxxxxxxxxxxxxxxxxxxx ---" # require a valid private key
authorize_scopes: "public_profile,email"
ProviderType: "SignInWithApple"
AttributeMapping:
email: "email"

In your are using a custom frontend instead of the hosted UI, iIn order to use the “Login with” button you will need to interact with social’s tokens, federated access, attributes mapping and lot of other stuff, not just a redirect.

The advice is to still activate the hosted UI even if you use a custom frontend and use that panel to manage authentication via social media in order to let Cognito do the dirty work, you just need to add the social’s related buttons.

Hosted UI

The AWS managed hosted UI implements all user access features, it is very useful in the testing phase to quickly add authentication to an existing application. Unfortunately, the lack of some imported features such as label translation, privacy policy management, custom field validation is not something that can be used in a production environment.

You can use your own domain to serve Hosted UI endpoints, not just the login/registration UI but also the exposed OAuth2 endpoints.

UserPoolDomain: 
Type: AWS::Cognito::UserPoolDomain
Properties:
UserPoolId: !Ref UserPool
Domain: !Sub "${Project}-${Environment}"

Customize Hosted UI

UserPoolUICustomization: 
Type: AWS::Cognito::UserPoolUICustomizationAttachment
DependsOn: UserPoolDomain
Properties:
UserPoolId: !Ref UserPool
ClientId: 'ALL'
CSS: |
.banner-customizable {
background-color: #6E6F6E;
padding: 2em 0;
}
.background-customizable {
background-color: #FAFAFA;
}
.submitButton-customizable {
background-color: #6E6F6E;
}
.submitButton-customizable:hover {
background-color: #6E6F6E;
}
.inputField-customizable {
border-color: #FAFAFA;
}
.inputField-customizable:focus {
border-color: #6E6F6E;
}

You cannot use it on all elements and you can only use some properties for some specific elements, see the AWS documentation for more details.

Frontend implementation

Install Amplify auth module, you don’t need the entire library

npm install @aws-amplify/auth

Import the auth module and configure it

import { Auth } from '@aws-amplify/auth'

Auth.configure({
region: "eu-south-1",
userPoolId: "eu-south-1_xxxxxxxxxxx",
userPoolWebClientId: "xxxxxxxxxxxxxxxxxxxxxxxxx",
})

Use the Hosted UI login/registration form to handle users authentication


import { CognitoHostedUIIdentityProvider, Auth } from '@aws-amplify/auth'

await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Cognito })

Implements self-hosted login form using auth module APIs and integrate the authentication functionalities with your own custom UI

import { Auth } from '@aws-amplify/auth'

await Auth.signUp({
username,
password,
attributes: {
email
}
})

await Auth.confirmSignUp(username, code)

await Auth.signIn(username, password)

Login with configured social provider using the hosted UI to manage the various redirects.

import { CognitoHostedUIIdentityProvider, Auth } from '@aws-amplify/auth'

await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google })
await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Facebook })
await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Apple })
await Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Amazon })

--

--