AWS Security Blog

How to configure Duo multi-factor authentication with Amazon Cognito

October 23: This post has been updated to utilize Duo Web v4 SDK and OIDC approach for integration with Duo two-factor authentication.


Adding multi-factor authentication (MFA) reduces the risk of user account take-over, phishing, and password theft. Adding MFA while providing a frictionless sign-in experience requires you to offer a variety of MFA options that support a wide range of users and devices. Let’s see how you can achieve that with Amazon Cognito and Duo MFA.

Amazon Cognito user pools are user directories that are used by Amazon Web Services (AWS) customers to manage the identities of their customers and to add sign-in, sign-up and user management features to their customer-facing web and mobile applications. Duo Security is an APN Partner that provides unified access security and multi-factor authentication solutions.

In this blog post, I show you how to use Amazon Cognito custom authentication flow to integrate Duo MFA into your sign-in flow and offer a wide range of MFA options to your customers. Some second factors available through Duo MFA are mobile phone SMS passcodes, approval of login via phone call, push-notification-based approval on smartphones, biometrics on devices that support it, and security keys that can be attached via USB.

How it works

Amazon Cognito user pools enable you to build a custom authentication flow that authenticates users based on one or more challenge/response cycles. You can use this flow to integrate Duo MFA into your authentication as a custom challenge.

Duo Web offers a software development kit to make it simpler for you to integrate your web applications with Duo MFA. You need an account with Duo and an application to protect (which can be created from the Duo admin dashboard). When you create your application in the Duo admin dashboard, note the client ID, client secret, and API hostname. These details are the primary factors used to integrate your Amazon Cognito user pool with Duo MFA.

Note: Client ID and client secret are referred to as Duo keys.

Duo MFA will be integrated into the sign-in flow as a custom challenge. To do that, you need to generate a redirect URL and a state value using Duo APIs and use those to load Duo MFA and request the user’s second factor. When the challenge is answered by the user, a response with duo_code and state value is returned to your application’s callback URL and sent to Amazon Cognito for verification. If the response is valid, then the MFA challenge is successful.

Let’s take a closer look at the sequence of calls and components involved in this flow.

Implementation details

In this section, I walk you through the end-to-end flow of integrating Duo MFA with Amazon Cognito using a custom authentication flow. To help you with this integration, I built a demo project that provides deployment steps and sample code to create a working demo in your environment.

Create and configure a user pool

The first step is to create the AWS resources needed for the demo. You can do that by deploying the AWS CloudFormation stack as described in the demo project.

A few implementation details to be aware of:

  • The template creates an Amazon Cognito user pool, application client, and AWS Lambda triggers that are used for the custom authentication.
  • The template also accepts the Duo client ID, client secret, and Host API name as inputs. For security, the parameters are masked in the AWS CloudFormation console. These parameters are stored in a secret in AWS Secrets Manager with a resource policy that allows the relevant Lambda functions read access to that secret.
  • Duo keys are loaded from Secrets Manager at the initialization of create auth challenge and verify auth challenge Lambda triggers to be used to create redirect_url and verify duo_code.

Authentication flow

Figure 1: User authentication process for the custom authentication flow

Figure 1: User authentication process for the custom authentication flow

The preceding sequence diagram (Figure 1) illustrates the sequence of calls to sign in a user, which are as follows:

  1. In your application, the user is presented with a sign-in UI that captures the username and password and starts the sign-in flow. A script—running in the browser—starts the sign-in process using the Amazon Cognito authenticateUser API with CUSTOM_AUTH set as the authentication flow. This validates the user’s credentials using Secure Remote Password (SRP) protocol and moves on to the second challenge if the credentials are valid.
  2. Note: The authenticateUser API automatically starts the authentication process with SRP. The first challenge that’s sent to Amazon Cognito is SRP_A. This is followed by PASSWORD_VERIFIER to verify the user’s credentials.

  3. After the SRP challenge step, the define auth challenge Lambda trigger returns CUSTOM_CHALLENGE and this moves control to the create auth challenge trigger.
  4. The create auth challenge Lambda trigger first builds a Duo auth client using the Duo keys, Duo Host API, and application callback URL, then uses the Duo auth client to generate a Duo auth state and an auth URL with the username. This Lambda trigger returns the auth URL and state value as a challenge to the client. Here is a sample code of what create auth challenge should look like:
  5. export const handler = async (event) => {
    
    	//Load Duo keys from secrets manager
    
    	if (clientId == null || clientSecret == null || apiHost == null) {
    
    		console.log("----------------Loading duo keys");
    
    		try {
    
    			const data = await smClient.send(new GetSecretValueCommand({ SecretId: secretName }));
    
    			if ('SecretString' in data) {
    
    				secret = JSON.parse(data.SecretString);
    
    				clientId = secret['duo-clientid'];
    
    				clientSecret = secret['duo-clientsecret'];
    
    				apiHost = secret['duo-apihostname'];
    
    			}
    
    		}
    
    		catch (err) {
    
    			console.error(err);
    
    			throw err;
    
    		}
    
    	}
    
    	const client = new Client({
    
    		clientId,
    
    		clientSecret,
    
    		apiHost,
    
    		redirectUrl,
    
    	});
    
    	const status = await client.healthCheck();
    
    	const state = client.generateState();
    
    	//create sign request using duo SDK
    
    	let username = event.userName;
    
    	const authUrl = client.createAuthUrl(username, state);
    
    	event.response.publicChallengeParameters = {
    
    		authUrl: authUrl,
    
    		username,
    
    		state
    
    	};
    
    	return event;
    
    };
  6. The client stores the Duo state value and then redirects the user to the returned Duo auth URL. This is done on the client side as follows:
  7. //render Duo MFA, redirect to Duo URL
    		window.location.href = challengeParameters.authUrl;
    		localStorage.setItem('duomfa-cognito', JSON.stringify({
    			username: challengeParameters.username,
    			state: challengeParameters.state,
    			session: cognitoUser.Session
    		 }));
  8. Through the redirected Duo page, the user can set up their MFA preferences and respond to an MFA challenge. After a successful MFA setup, a Duo Auth code and the state value from the Duo Web library will be returned to the client application’s callback URL that was provided in duo_universal’s new client call in the create auth challeng Lambda trigger.
  9. Figure 2: The first time a user signs in, Duo MFA displays a Start setup screen

    Figure 2: The first time a user signs in, Duo MFA displays a Start setup screen

  10. The client sends the Duo auth code response to Amazon Cognito as a challenge response.
  11. Amazon Cognito sends the response to the verify auth challenge Lambda trigger, which uses Duo keys and username to verify the response.
  12. export const handler = async (event) => {
    
    	if (clientId == null || clientSecret == null || apiHost == null) {
    
    		try {
    
    			const data = await smClient.send(new GetSecretValueCommand({ SecretId: secretName }));
    
    			if ('SecretString' in data) {
    
    				secret = JSON.parse(data.SecretString);
    
    				clientId = secret['duo-clientid'];
    
    				clientSecret = secret['duo-clientsecret'];
    
    				apiHost = secret['duo-apihostname'];
    
    			}
    
    		}
    
    		catch (err) {
    
    			console.error(err);
    
    			throw err;
    
    		}
    
    	}
    
    	var username = event.userName;
    
    	//-------get challenge answer
    
    	const duoCode = event.request.challengeAnswer;
    
    	const client = new Client({
    
    		clientId,
    
    		clientSecret,
    
    		apiHost,
    
    		redirectUrl,
    
    	});
    
    	try {
    
    		const verificationResult = await client.exchangeAuthorizationCodeFor2FAResult(duoCode, username);
    
    		event.response.answerCorrect = true;
    
    		return event;
    
    	} catch (err) {
    
    		event.response.answerCorrect = false;
    
    		return event;
    
    	}
    
    };
  13. Validation results and current state are passed once again to the define auth challenge Lambda trigger. If the user response is valid, then the Duo MFA challenge is successful. You can then decide to introduce additional challenges to the user or issue tokens and complete the authentication process.

Conclusion

As you build your mobile or web application, keep in mind that using multi-factor authentication is an effective and recommended approach to protect your customers from account take-over, phishing, and the risks of weak or compromised passwords. Making multi-factor authentication simple for your customers enables you to offer authentication experience that protects their accounts but doesn’t slow them down.

Visit the security pillar of AWS Well-Architected Framework to learn more about AWS security best practices and recommendations.

In this blog post, I showed you how to integrate Duo MFA with an Amazon Cognito user pool. Visit the demo application and review the code samples in it to learn how to integrate this with your application.

If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, start a new thread on the Amazon Cognito forum or contact AWS Support.

Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.

Author

Mahmoud Matouk

Mahmoud is a Senior Solutions Architect with the Amazon Cognito team. He helps AWS customers build secure and innovative solutions for various identity and access management scenarios.

Yungang Wu

Yungang Wu

Yungang is a Senior Cloud Support Engineer who specializes in the Amazon Cognito service. He helps AWS customers troubleshoot issues and suggests well-designed application authentication and authorization implementations.