Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to include TOTP MFA in AWS Cognito authentication process

I'm using Cognito user pools to authenticate my web application. I've got it all working right now but now I need to enable MFA for it. This is how I do it right now (all the code provided are server-side code):

  1. Signing up the user:
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.signUp({
    ClientId,
    Username: email,
    Password,
}).promise();
  1. An email is sent to the user's address (mentioned as username in the previous function call) with a code inside.

  2. The user reads the code and provides the code to the next function call:

cognito.confirmSignUp({
    ClientId,
    ConfirmationCode,
    Username: email,
    ForceAliasCreation: false,
}).promise();
  1. The user logs in:
const tokens = await cognito.adminInitiateAuth({
    AuthFlow: 'ADMIN_NO_SRP_AUTH',
    ClientId,
    UserPoolId,
    AuthParameters: {
        'USERNAME': email,
        'PASSWORD': password,
    },
}).promise();

I'm pretty happy with this process. But now I need to add the TOTP MFA functionality to this. Can someone tell me how these steps will be changed if I want to do so? BTW, I know that TOTP MFA needs to be enabled for the user pool while creating it. I'm just asking about how it affects my sign-up/log-in process.

like image 779
Mehran Avatar asked Jan 26 '23 10:01

Mehran


1 Answers

Alright, I found a way to do this myself. I must say, I couldn't find any documentation on this so, use it at your own risk!

Of course, this process assumes you have a user pool with MFA enabled (I used the TOTP MFA).

  1. Signing up the user:
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.signUp({
    ClientId,
    Username: email,
    Password,
}).promise();
  1. An email is sent to the user's address (mentioned as username in the previous function call) with a code inside.

  2. The user reads the code and provides the code to the next function call:

cognito.confirmSignUp({
    ClientId,
    ConfirmationCode: code,
    Username: email,
    ForceAliasCreation: false,
}).promise();
  1. The first log in:
await cognito.adminInitiateAuth({
    AuthFlow: 'ADMIN_NO_SRP_AUTH',
    ClientId,
    UserPoolId,
    AuthParameters: {
        'USERNAME': email,
        'PASSWORD': password,
    },
}).promise();

At this point, the return value will be different (compared to what you'll get if the MFA is not enforced). The return value will be something like:

{
  "ChallengeName": "MFA_SETUP",
  "Session": "...",
  "ChallengeParameters": {
    "MFAS_CAN_SETUP": "[\"SOFTWARE_TOKEN_MFA\"]",
    "USER_ID_FOR_SRP": "..."
  }
}

The returned object is saying that the user needs to follow the MFA_SETUP challenge before they can log in (this happens once per user registration).

  1. Enable the TOTP MFA for the user:
cognito.associateSoftwareToken({
  Session,
}).promise();

The previous call is needed because there are two options and by issuing the given call, you are telling Cognito that you want your user to enable TOTP MFA (instead of SMS MFA). The Session input is the one return by the previous function call. Now, this time it will return this value:

{
  "SecretCode": "...",
  "Session": "..."
}
  1. The user must take the given SecretCode and enter it into an app like "Google Authenticator". Once added, the app will start showing a 6 digit number which is refreshed every minute.

  2. Verify the authenticator app:

cognito.verifySoftwareToken({
  UserCode: '123456',
  Session,
}).promise()

The Session input will be the string returned in step 5 and UserCode is the 6 digits shown on the authenticator app at the moment. If this is done successfully, you'll get this return value:

{
  "Status": "SUCCESS",
  "Session": "..."
}

I didn't find any use for the session returned by this object. Now, the sign-up process is completed and the user can log in.

  1. The actual log in (which happens every time the users want to authenticate themselves):
await cognito.adminInitiateAuth({
    AuthFlow: 'ADMIN_NO_SRP_AUTH',
    ClientId,
    UserPoolId,
    AuthParameters: {
        'USERNAME': email,
        'PASSWORD': password,
    },
}).promise();

Of course, this was identical to step 4. But its returned value is different:

{
  "ChallengeName": "SOFTWARE_TOKEN_MFA",
  "Session": "...",
  "ChallengeParameters": {
    "USER_ID_FOR_SRP": "..."
  }
}

This is telling you that in order to complete the login process, you need to follow the SOFTWARE_TOKEN_MFA challenge process.

  1. Complete the login process by providing the MFA:
cognito.adminRespondToAuthChallenge({
  ChallengeName: "SOFTWARE_TOKEN_MFA",
  ClientId,
  UserPoolId,
  ChallengeResponses: {
    "USERNAME": config.username,
    "SOFTWARE_TOKEN_MFA_CODE": mfa,
  },
  Session,
}).promise()

The Session input is the one returned by step 8 and mfa is the 6 digits that need be read from the authenticator app. Once you call the function, it will return the tokens:

{
  "ChallengeParameters": {},
  "AuthenticationResult": {
    "AccessToken": "...",
    "ExpiresIn": 3600,
    "TokenType": "Bearer",
    "RefreshToken": "...",
    "IdToken": "..."
  }
}
like image 193
Mehran Avatar answered Mar 03 '23 12:03

Mehran