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).
- Signing up the user:
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.signUp({
ClientId,
Username: email,
Password,
}).promise();
An email is sent to the user's address (mentioned as username in the previous function call) with a code inside.
The user reads the code and provides the code to the next function call:
cognito.confirmSignUp({
ClientId,
ConfirmationCode: code,
Username: email,
ForceAliasCreation: false,
}).promise();
- 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).
- 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": "..."
}
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.
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.
- 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.
- 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": "..."
}
}
ChallengeName: "MFA_SETUP"
, theUSERNAME
inChallengeResponses
, and the Session from VerifySoftwareToken – Sextain