This content originally appeared on Level Up Coding - Medium and was authored by Oliver Schenk
Includes a working Angular SPA sample app
Amazon Cognito is a secure and scalable service to manage sign-up, sign-in and access control for mobile and web apps. There are many features and the full breadth of the service can be discovered in the documentation.
In this article, we are going to create complete solution that demonstrates user sign-up and sign-in through an Angular front-end app and a Cognito User Pool back-end deployed using Terraform.
The source code is available in Github.
It is worth noting that Cognito provides a lot of functionality, flexibility and customisation. However, this also comes at the cost of complexity.
Rather than trying to dive into every possible feature, it is usually best to think about what it is that you want to achieve in your app and then research from there. Think business process, then data and application requirements and finally choose the technology that meets those requirements. Cognito is of course not the only solution on the market — there’s also Auth0, Okta, Firebase, Azure AD, etc…
As an alternative approach, AWS provides a service called AWS Amplify, which, alongside a whole raft of common app features, includes user management and authentication as one of its abstractions. It is designed to hide away the complexities of Cognito and promote a rapid development style.
The cool thing is that we can still leverage the power of the Amplify libraries without actually having to go for the full Amplify service. In this article we will make use of the AWS Amplify Auth library.
What we will build
The features that we are looking to implement include the following:
- Self-service user sign-up based on phone number
- Account activation using SMS verification code
- User sign-in and sign-out
- Custom user attributes
- View user profile
- Password reset
Solution architecture
The solution consists of two main parts. The first is the back-end Cognito infrastructure deployed in AWS using Terraform and the second is the front-end Angular app that will interact with the Cognito service.
A diagram of what will be built in this solution and what processes it will support is shown below.
Back-end
The primary component in the back-end is a Cognito User Pool and the associated configuration to support the features we’ve specified above.
Cognito can be configured for many different use cases. Any technology that requires strong, yet customisable, security will inevitably have a higher degree of complexity both conceptually and practically.
All of the features that we want in regards to unauthenticated and authenticated user management can be achieved using the following AWS resources:
- Cognito User Pool
This is the primary user directory resource in Cognito. - Cognito User Pool App Client
This is represents a client application that can make Cognito API calls as an unauthenticated client such as registering, sign-in and password resets. In our case the user pool client is the Angular front-end app. - SMS Role
This is a role that Cognito will assume to send an SMS via Amazon SNS.
We will go into each of the above in more detail in the implementation below.
Front-end
The front-end represents the user facing application. In this guide the front-end has been created as a demonstration of the features that relate specifically to interactions with the Cognito service.
The application in this guide was written using Angular, however the same principles apply to any “public” client that runs in a browser or mobile app. Cognito also supports app clients that can be considered “confidential” such as server-side rendered applications or back-end scripts.
The application uses a custom UI design rather than the Cognito Hosted UI. This means the communication with Cognito has to be handled manually. This was achieved using the AWS Amplify Auth library, which under the hood makes use of the amazon-cognito-identity-js library.
Prerequisites
- Terraform
- Node.JS
- [optional] aws-vault (for credentials management)
- [optional] Redux DevTools Chrome extension (for visualising NgRx actions and state)
- A verified phone number in the SMS Sandbox in your AWS account
Back-end implementation
Alright, let’s get onto building some infrastructure using Terraform. This section assumes that you are at least somewhat familiar with Terraform and how files are generally defined and organised.
In the root of your repo create the backend folder with some some empty files.
mkdir backend
cd backend
touch variables.tf main.tf outputs.tf provider.tf
First of all let’s set up the provider in the provider.tf file. If you want to load credentials from a specific profile you can provide the profile = <credentials_profile> in the provider block. However, for better security you might want to look at something like aws-vault and assume roles to access resources in the desired account.
Now define some variables in variables.tf.
Now let’s define our main resources in main.tf.
Finally, define the outputs in outputs.tf, which are needed to configure the front-end.
Locals
In the locals section of the main.tf file I’ve defined an id that is used throughout for resource naming purposes. You can name your resources whatever you want by adjusting the project_name and stage variables. It is good practice to include a stage variable as it clearly shows whether a resource is, for example, dev or prod.
Cognito user pool
The User Pool in Cognito represents the main user directory. This is where users are created and managed. The following list highlights the important aspects of the configuration:
- The username_attributes property allows you to specify which user pool attributes are allowed to be used as the username when signing up. This can be email, phone_number, both (meaning either can be used, but then your app will need more sophisticated logic for authentication) or empty (meaning your username is explicitly provided by your users as a string). In our case we want the user to sign-up using their phone number so we have set this to phone_number.
- In apps where users are able to register themselves, the email address and/or phone number is often a required field and is then verified in order to help mitigate against bogus or fake accounts being activated. In Cognito there are a few ways to achieve this. One way is to implement a pre sign-up Lambda trigger and write your own logic to send out and handle the verification of a user. The other way is to use Cognito’s auto_verified_attributes property to let Cognito send out the necessary verification email or SMS depending on whether email or phone_number is specified. In our case we want this to be set to phone_number so that Cognito deals with creating a verification code and then uses SNS to send an SMS to the given phone number.
- To ensure that users are allowed to register themselves, rather than just via the admin console or the API, the allow_admin_create_user_only property should be set to false.
- For an explanation of the sms_configuration configuration block see the SNS role section below.
- The password_policy configuration block defines the password policy and the lifetime of any temporary password or code. This is documented here.
- The account_recovery_setting configuration block is used to define password recovery mechanism. In our case the name property should be set to verified_phone_number to ensure that the user can recover their password using their verified phone number.
- The schema configuration blocks are used to define the user attributes. This includes both standard and custom attributes. Note that in the Terraform source code it is not possible to tell which attributes are standard attributes and which are custom attributes. However, once the user pool is created the custom attributes will automatically be prefixed by custom:in Cognito and need to be referenced as such in our code.
SNS IAM role
In order to allow Cognito to send SMS messages it must be allowed to assume a role that includes the relevant policy for publishing via Amazon SNS. The ARN of the role is given in the sms_configuration configuration block sns_caller_arn of the user pool.
For additional security, the role is configured with an sts:ExternalId parameter that is also given to the user pool sms_configuration object via the external_id parameter. What this means is that when Cognito assumes the role it will pass the ExternalID parameter, which will then be validated against the role’s expected ExternalID. In our case the external ID is randomly generated by Terraform using the random_string resource.
Cognito user pool client
A Cognito user pool supports a variety of API calls. Some of these API calls can be performed via an application in an unauthenticated user state. Sign-up, email/phone number confirmation and password reset are examples of such operations. These calls must be done within the context of a Cognito user pool client by providing the Client ID as a parameter in an API call.
The purpose of a user pool client configuration is to specify what authentication flows are allowed to be used by a particular client, how token expiry is managed, what callback URLs are used and other OAuth related settings.
A user pool can have many user pool client configurations attached to it. This is because even though different types of apps might access the same user pool, they can, and probably will, have different authentication requirements depending on the nature of the app (e.g. front-end or back-end, third party integration, etc…)
The following choices were made for the solution being built in this guide:
- The explicit auth flows are set to ALLOW_USER_SRP_AUTH and ALLOW_REFRESH_TOKEN_AUTH. This allows for a secure username and password authentication method as well as refresh tokens to obtain new tokens when existing tokens have expired.
- Setting the parameter enable_token_revocation to true allows for token revocation via a number of methods as documented.
- Setting the parameters prevent_user_existence_errors to ENABLED ensures that error messages do not give away the fact that a user does not exist. This is documented further here. It is more secure to simply say “incorrect username or password” than to say “user not found”, because it allows real phone numbers to be discovered.
Testing
To test the Cognito User Pool we can use the AWS CLI. The first thing we can do is try to register a new user.
Note that I’m using aws-vault for credentials management.
aws-vault exec OliverSchenk-Admin-Dev -- aws cognito-idp sign-up --client-id 9ocqafeo835vplo3lns7pc7ub --username +61400000000 --password TestPassword1 --user-attributes Name="name",Value="Oliver Schenk" Name="custom:company",Value="ACME" Name="custom:role_name",Value="Inventor"
- Replace the client-id value with your own client ID
- Replace the username value with your own test phone number
- Replace the password value with whatever password you want
- Replace the other values for name, custom:company and custom:role_name with whatever values you want
At this point you should get an SMS message on your registered sandbox phone number containing a 6 digit code. If not, check your phone number includes the country code and that your AWS account has the SMS sandbox enabled.
You can now verify the phone number by providing the code with another AWS CLI command.
aws-vault exec OliverSchenk-Admin-Dev -- aws cognito-idp confirm-sign-up --client-id 9ocqafeo835vplo3lns7pc7ub --username +61400000000 --confirmation-code 123456
- Replace the client-id value with your own client ID
- Replace the username value with your own phone number
- Replace the confirmation code with the code you received in the SMS message
At this point the user is now verified.
We will not be able to easily test the authentication part using the AWS CLI, because the configured authentication method uses SRP which is not easily calculated without using code and a library. In the next section we’ll see how to use the amazon-cognito-identity-js library to interact with the Cognito user pool.
Front-end implementation
Let’s now focus on the front-end. To make things a little bit more interesting I used the Ionic framework and NgRx for reactive style app state management. You will find the entire source code in the GitHub repo.
I’m not going to paste every single source code file in the article body, but I provided some of the important parts that relate to the Cognito implementation and anything else that I encountered that I had to resolve.
Cognito user pool and Cognito client ID
As an output of the Cognito User Pool and User Pool Client resources created using Terraform you will get a Cognito user pool ID and a Cognito user pool client ID. These should be provided in the environment.ts file as follows. You should replace these with your own details.
You will most likely have a dev and prod stage at least and therefore the production Cognito IDs would go into environment.prod.ts.
Running the sample app
You can run the sample app by installing Ionic CLI and serving up the app using the following commands.
npm install -g @ionic/cli
cd frontend/aws-cognito-angular
ionic serve
It is best to use the browser’s development tool and run the application in mobile app view size or resize the browser window.
Auth module
All of the authentication related logic and UI is contained within the authentication module stored in the src\app\core\auth folder. This consists of the following:
- components — The Angular reusable components used by the auth pages.
- pages — The Angular components that represent Ionic pages.
- services — The main auth services and guards.
- state — The NgRx actions, effects, models, reducers and selectors for authentication actions and page state actions.
- auth.module — The Angular module definition and the root of the NgRx imports for the auth feature store and effects.
- auth.routes — The page routing definition.
Auth service
The interaction with Cognito in this sample app is being handled through a service that makes use of the @aws-amplify/auth library and the documentation on the Amplify website.
The service provides a uniform authentication and user management API that hides a few of the Cognito details and communications. It provides all the standard functions that would be expected from an app including sign-up, sign-in, sign-out, password management and user profile management.
Effects and reducers
Whilst the auth service is responsible for providing a simplified API to communicate with Cognito, the NgRx effects are responsible for handling any actions that are dispatched from the UI and from other effects.
For example, the sign-in page dispatches a signIn action when the Login button is clicked. This is handed by the signIn$ effect. This in turn calls the authService.signIn method and, if the result is successful, dispatches the signInSuccessful action. This in turn is handled by the signInSuccessful$ effect, which navigates to the root path of the site (e.g. the profile page).
As each action is dispatched, the reducers also set the relevant state. For example loading, showError and isLoggedIn. These states are then reflected in the UI dynamically as they are all Observables.
Profile module
The profile module is responsible for the main profile page and the profile edit page. You can look at the source code to understand this part of the app. I this secured part of the app rather simple as the main focus on in this article are the Cognito related operations.
The profile pages were added to demonstrate a protected page using an AuthGuard and the currently authenticated user to make Cognito API calls. The profile data itself is read and updated using the Cognito API via the profile and auth services.
Tips
Below are various tips regarding issues that I encountered with Angular when working through this solution.
Tip 1 — Fix EsLint error
In order to prevent eslint from showing errors in every Typescript source code file I had to follow this information on Stack Overflow in order to resolve this issue. I had to rename .eslintrc.json to .eslintrc.js and then in that file add the following to the parserOptions configuration:
“tsconfigRootDir”: __dirname
Tip 2 — Fix ‘global is not defined’ error
When serving up the ionic project you will most likely get an error in the browser as follows:
Uncaught ReferenceError: global is not defined
To resolve this add the following at the end of the polyfills.ts file:
(window as any).global = window;
Tip 3 — CommonJS and AMD build warnings
When building or serving the Angular SPA a number of warnings appear due to the use of the AWS Amplify Auth library. These can be muted in angular.json by adding the following to the build options:
"options": {
...
"allowedCommonJsDependencies": [
"crypto-js",
"buffer",
"isomorphic-unfetch",
"url",
"@aws-crypto/sha256-js",
"@aws-crypto/sha256-browser",
"uuid"
]
}
Tip 4— Redux DevTools
Make sure you make use of the Redux DevTools Chrome Extension to view the state of the NgRx Store as various actions are performed.
Level Up Coding
Thanks for being a part of our community! More content in the Level Up Coding publication.
Follow: Twitter, LinkedIn, Newsletter
Level Up is transforming tech recruiting ➡️ Join our talent collective
Amazon Cognito with SMS user verification was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Oliver Schenk
Oliver Schenk | Sciencx (2022-06-22T15:17:18+00:00) Amazon Cognito with SMS user verification. Retrieved from https://www.scien.cx/2022/06/22/amazon-cognito-with-sms-user-verification/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.