How to include TOTP MFA in AWS Cognito authentication process - amazon-cognito

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):
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,
Username: email,
ForceAliasCreation: false,
}).promise();
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.

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": "..."
}
}

Related

Way to verify username and password of user in aws cognito using adminInitiateAuth() method

Requirement:
Below code is having 2 functions. 1st verify the username and password of user and if it is true it trigger OTP in SMS(Default behavior of AWS as 2 factor authentication is enabled). But we do not want OTP in SMS. We want OTP in Email with custom template, so implemented 2nd function with AuthFlow: 'CUSTOM_AUTH'(and 2nd method works as expected).
We do not want OTP to be triggered in SMS(But also can not disable 2 factor auth because it is used in other use cases). Also, only need solution using aws-sdk. There are ways using amplify and other library but it is not useful in case of App client secret is there.
//verify username,password and send code in sms
response0 = await cognitoIdentityServiceProvider.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId: tenant.cognitoClientId,
UserPoolId: tenant.cognitoUserPool,
AuthParameters: {
SECRET_HASH: crypto.createHmac('SHA256', tenant.cognitoClientSecret).update(username + tenant.cognitoClientId).digest('base64'),
USERNAME: username,
PASSWORD: password
}
}).promise();
// send code to email using custom auth flow
response1 = await cognitoIdentityServiceProvider.adminInitiateAuth({
AuthFlow: 'CUSTOM_AUTH',
ClientId: tenant.cognitoClientId,
UserPoolId: tenant.cognitoUserPool,
AuthParameters: {
SECRET_HASH: crypto.createHmac('SHA256', tenant.cognitoClientSecret).update(username + tenant.cognitoClientId).digest('base64'),
USERNAME: username,
PASSWORD: tenantId + secrets.PASSWORD_SECRET
}
}).promise();
Need solution where we can check username password using AuthFlow: 'CUSTOM_AUTH'(Can change lambda triggers) or any other way where OTP should not be triggered and able to check username and password correctly.

What to use as password when registering a user via Google OAuth?

I have a sign-up page that uses email and password.
I store the email and (salted, hashed) password in the database.
I'm now adding Google OAuth to my sign-up page.
For the main advantage being that user now shouldn't have to pick a password.
When the user authenticates and I receive the user's Google profile, what exactly do use now as their "password"? The profile object looks like this:
{
Ca: '123456789...',
wc: {
token_type: 'Bearer',
access_token: 'abc.123.ABCabc..........',
scope: 'email profile ...',
login_hint: '...',
expires_in: 3599,
id_token: '......',
session_state: { ... },
first_issued_at: ...,
expires_at: ...,
...
},
googleId: ...,
tokenObj: { ... },
profileObj: { googleId: ..., name: ..., },
}
I was inclined to use "access_token" or "id_token" as password but then there's "expires_at" field that suggests they might change over time and be of no use in future login attempts.
Would they? If so, what else should I choose as the user's password that won't change and allow to me authenticate them every time they login via Google OAuth?

Email only authentication with Vue.js and Vuex on Firebase

I want user to be automatically authenticated (temporarily) on Firebase just by sending Email then be redirected to a welcome page asking to complete the auth process by following a link received by email.
The first part is ok, I can authenticate by just inserting email and generating a random password like the following (Vuex store action):
this.$store.dispatch('userSignUp', { email: this.email, password: this.generatePassword })
which is called by component method button v-on:click="userSignUp
Vuex action is like :
userSignUp ({commit}, payload) {
commit('setLoading', true)
firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
commit('setError', null)
router.push('/welcome')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
})
}
So far so good, the user put the email, an helper function this.generatePassword generate a random password and the user is logged in and created on firebase.
Now this user is logged in, is on a welcome page, but it doesn't know his own random password (because I don't want to).
I want this to be one shot login and if the user want to come back, has to follow the link sent by email by Firebase.
There is a firebase function [sendPasswordResetEmail][1], which seems right for the case but I connot find the way to make it working.
I did Vuex action like before :
export const actions = {
sendPasswordReset ({commit}, payload) {
commit('setLoading', true)
firebase.auth().sendPasswordResetEmail(payload.email)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
commit('setError', null)
router.push('/welcome')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
router.push('/error')
})
},
...
which is called by component method button v-on:click="userSignUp
methods: {
userSignUp () {
this.$store.dispatch('userSignUp', { email: this.email, password: this.generatePassword })
this.$store.dispatch('sendPasswordReset', { email: this.email })
}
},
I only get response code
{
"error": {
"errors": [
{
"domain": "global",
"reason": "invalid",
"message": "EMAIL_NOT_FOUND"
}
],
"code": 400,
"message": "EMAIL_NOT_FOUND"
}
}
while the Request Payload seems ok anyway :
{requestType: "PASSWORD_RESET", email: "luca.soave#gmail.com"}
email
:
"luca.soave#gmail.com"
requestType
:
"PASSWORD_RESET"
Any idea ?
The provider you're using is called the password provider. As its name implies it is heavily dependent on the user having (and knowing) a password. Since you are looking for passwordless authentication, I'd recommend against using the email+password provider as the basis.
Instead consider implementing a custom authentication provider. While this involves a few more components, it is not as difficult as you may think. You'll need to run trusted code, which you can do either on a server you already have, or on Cloud Functions. In either of those cases, you'll use one of the Admin SDKs to implement the sensitive parts of the authentication flow.
A quick list of steps that I think you'll need:
Create an endpoint (e.g. a HTTP triggered Cloud Function) for the user to request an authentication email.
Implement the code for this endpoint to:
Generate a random one-time code in there, which you're going to send to the user. Firebase Authentication calls this the out-of-band (or OOB) code, since it's sent to the user on a different medium than your app.
Store this code and the user's email address somewhere where only your server-side code can read it, e.g. in the Firebase Database or Cloud Firestore.
Send an email to the user, with the code or a link to a page that includes the code and their email address.
Create an endpoint (e.g. again a HTTP function, or web page) where the user enters (e.g. by clicking on a link in the email) the OOB code and their email address.
Compare the code the user entered, to the one you stored before.
If the codes match, generate a custom token for the user and send it to them.
The user/app now signs into Firebase with the custom token.

Meteor Accounts obtaining the SAML Response (Token / User after log in)

i have a workin SAML logon with Meteor.accounts and MS Azure. I am using this https://github.com/steffow/meteor-accounts-saml library for SAML which is derived from https://github.com/bergie/passport-saml
The procedure goes like this:
Click saml login -> popup appears
Enter user/pw data in popup -> popup closes without error
Logged in success
So now i want to get the SAML Token for further processing (or at least the information about the logged in user which meteor has taken from the IDP).
Since i dont have a clue where the SAML Token is stored by Meteor or can be fetched in the code, can someone help me getting the SAML Response?
Probably solved by now but here it goes..
According to the code the saml response is retrieved in "saml_server.js" at row 70 and the token should be in there also (loginRequest.credentialToken)
loginResult should be fairly easy to save into the Meteor.users collection
var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
var email = loginResult.profile.email;
var user = Meteor.users.findOne({username: email});
if (!user) {
Accounts.createUser({
"username": email,
"profile": StufffromloginResult
});
} else {
Meteor.users.update(
{
"username": email
},
{ "$set": {
"profile": StufffromloginResult,
}
});
}
And retrieved with:
Meteor.user().profile

How to create a custom user authentication in Meteor?

I am trying to create the following authentication for an app:
User enters phone number and receives an SMS with a code generated in the server (the SMS is handled through an external service). If the user enters the right code he is logged in.
This means I must have two login stages: registering user with a phone and logging him in with the code, so this is what I think the client should look like:
Meteor.getSmsCode = function(phone, username, callback) {
Accounts.callLoginMethod({
methodName: 'getsmscode',
methodArguments: [{
getsmscode: true,
phone: phone,
username: username
}],
userCallback: callback
});
};
Meteor.loginWithCode = function(phone, code, callback) {
Accounts.callLoginMethod({
methodName: 'login',
methodArguments: [{
hascode: true,
phone: phone,
code: code
}],
userCallback: callback
});
};
But I am confused about the server side - there should be two methods:
the first should only register a user (and communicate with the SMS service) and second should log him in.
This is the server test code for now:
Meteor.users.insert({phone: '123456789', code: '123', username:'ilyo'});
Accounts.registerLoginHandler(function(loginRequest) {
var user = Meteor.users.findOne({phone: loginRequest.phone});
if(user.code !== loginRequest.code) {
return null;
}
var stampedToken = Accounts._generateStampedLoginToken();
var hashStampedToken = Accounts._hashStampedToken(stampedToken);
Meteor.users.update(userId,
{$push: {'services.resume.loginTokens': hashStampedToken}}
);
return {
id: user._id,
token: stampedToken.token
};
});
And this is what happens when I try it:
Why an I getting the 500?
Why doesn't the user have a code and phone fields?
What method should I use for the getSmsCode?
Meteor.createUser is described on How can I create users server side in Meteor?
Then, the Accounts.onCreateUser would contain business logic http://docs.meteor.com/#accounts_oncreateuser
A more exact message for the 500 would be on the server-side stdout. Probably security.
Your Login Handler must return an object as follows:
{ userId: user._id }
Sorry I don't elaborate in the whole problem, I don't agree on your full approach but looks you are in the right path to get the feature you need.
Also, this question is one year old, now there are a few packages at atmosphere that address this kind of authentication =)