AWS cognito user migration pool trigger not working on login flow - amazon-web-services

I am using AWS cognito pool migration using Lambda function with cognito execution role
Following is my new pool app client setting
or
AWS doc says
User migration authentication flow A user migration Lambda trigger
allows easy migration of users from a legacy user management system
into your user pool. To avoid making your users reset their passwords
during user migration, choose the USER_PASSWORD_AUTH authentication
flow. This flow sends your users' passwords to the service over an
encrypted SSL connection during authentication.
When you have completed migrating all your users, we recommend
switching flows to the more secure SRP flow. The SRP flow does not
send any passwords over the network.
I have created lambda function with role "AmazonCognitoPowerUser"
async function authenticateUser(cognitoISP: CognitoIdentityServiceProvider, username: string, password: string): Promise<User | undefined> {
console.log(`authenticateUser: user='${username}'`);
const params: AdminInitiateAuthRequest = {
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
AuthParameters: {
PASSWORD: password,
USERNAME: username,
},
ClientId: OLD_CLIENT_ID,
UserPoolId: OLD_USER_POOL_ID,
};
const cognitoResponse = await cognitoISP.adminInitiateAuth(params).promise();
const awsError: AWSError = cognitoResponse as any as AWSError;
if (awsError.code && awsError.message) {
console.log(`authenticateUser: error ${JSON.stringify(awsError)}`);
return undefined;
}
console.log(`authenticateUser: found ${JSON.stringify(cognitoResponse)}`);
return lookupUser(cognitoISP, username);
}
async function lookupUser(cognitoISP: CognitoIdentityServiceProvider, username: string): Promise<User | undefined> {
console.log(`lookupUser: user='${username}'`);
const params = {
UserPoolId: OLD_USER_POOL_ID,
Username: username,
};
const cognitoResponse = await cognitoISP.adminGetUser(params).promise();
const awsError: AWSError = cognitoResponse as any as AWSError;
if (awsError.code && awsError.message) {
console.log(`lookupUser: error ${JSON.stringify(awsError)}`);
return undefined;
}
console.log(`lookupUser: found ${JSON.stringify(cognitoResponse)}`);
const userAttributes = cognitoResponse.UserAttributes ? cognitoResponse.UserAttributes.reduce((acc, entry) => ({
...acc,
[entry.Name]: entry.Value,
}), {} as {[key: string]: string | undefined}) : {};
const user: User = {
userAttributes,
userName: cognitoResponse.Username,
};
console.log(`lookupUser: response ${JSON.stringify(user)}`);
return user;
}
async function onUserMigrationAuthentication(cognitoISP: CognitoIdentityServiceProvider, event: CognitoUserPoolTriggerEvent) {
// authenticate the user with your existing user directory service
const user = await authenticateUser(cognitoISP, event.userName!, event.request.password!);
if (!user) {
throw new Error('Bad credentials');
}
event.response.userAttributes = {
// old_username: user.userName,
// 'custom:tenant': user.userAttributes['custom:tenant'],
email: user.userAttributes.email!,
email_verified: 'true',
preferred_username: user.userAttributes.preferred_username!,
};
event.response.finalUserStatus = 'CONFIRMED';
event.response.messageAction = 'SUPPRESS';
console.log(`Authentication - response: ${JSON.stringify(event.response)}`);
return event;
}
async function onUserMigrationForgotPassword(cognitoISP: CognitoIdentityServiceProvider, event: CognitoUserPoolTriggerEvent) {
// Lookup the user in your existing user directory service
const user = await lookupUser(cognitoISP, event.userName!);
if (!user) {
throw new Error('Bad credentials');
}
event.response.userAttributes = {
// old_username: user.userName,
// 'custom:tenant': user.userAttributes['custom:tenant'],
email: user.userAttributes.email!,
email_verified: 'true',
preferred_username: user.userAttributes.preferred_username!,
};
event.response.messageAction = 'SUPPRESS';
console.log(`Forgot password - response: ${JSON.stringify(event.response)}`);
return event;
}
export const handler = async (event: CognitoUserPoolTriggerEvent, context: Context): Promise<CognitoUserPoolTriggerEvent> => {
const options: CognitoIdentityServiceProvider.Types.ClientConfiguration = {
region: OLD_USER_POOL_REGION,
};
if (OLD_ROLE_ARN) {
options.credentials = new ChainableTemporaryCredentials({
params: {
ExternalId: OLD_EXTERNAL_ID,
RoleArn: OLD_ROLE_ARN,
RoleSessionName: context.awsRequestId,
},
});
}
const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider(options);
switch (event.triggerSource) {
case 'UserMigration_Authentication':
return onUserMigrationAuthentication(cognitoIdentityServiceProvider, event);
case 'UserMigration_ForgotPassword':
return onUserMigrationForgotPassword(cognitoIdentityServiceProvider, event);
default:
throw new Error(`Bad triggerSource ${event.triggerSource}`);
}
}
and added trigger into new pool,
After many attempt Lambda trigger is not working on login always getting error .
{__type: "NotAuthorizedException", message: "Incorrect username or password."}
message: "Incorrect username or password."
__type: "NotAuthorizedException"
Though its working fine if we use forget passwords flow after reset password user migrated to new pool
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-import-using-lambda.html
UPDATED :
When directly run test on lambda using following json
{
"version": "1",
"triggerSource": "UserMigration_Authentication",
"region": "ap-south-1",
"userPoolId": "ap-XXXXXXXXX2",
"userName": "vaquar.test#gmail.com",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "1XXXXXXXXXXXXXXXXXXfgk"
},
"request": {
"password": "vkhan",
"validationData": null,
"userAttributes": null
},
"response": {
"userAttributes": null,
"forceAliasCreation": null,
"finalUserStatus": null,
"messageAction": null,
"desiredDeliveryMediums": null
}
}
Then getting following response and user migrated into new pool means we have issue in trigger during login.
INFO Authentication - response: {"userAttributes":{"email":"vaquar.test#gmail.com","email_verified":"true"},"forceAliasCreation":null,"finalUserStatus":"CONFIRMED","messageAction":"SUPPRESS","desiredDeliveryMediums":null}

With Lambda trigger, Cognito service invokes Lambda function. So Cognito will require permission to invoke Lambda function. How are you configuring the Lambda trigger on your userpool? If you are using AWS Cognito console, the permission should be set automatically.
You may also verify if the user already exists in the new userpool that could cause such behavior.

Please verify that your application is using OAUTH flow USER_PASSWORD instead of the default USER_SRP_AUTH.
For reference: link

Related

how to get user confirmation status from AWS Cognito

I need getting an information about user confirmation status, using amazon-cognito-identity-js
Using this function i can't do it
export const getCognitoUser = (email) => {
const userData = {
Username: email,
Pool: getUserPool()
}
return new AmazonCognitoIdentity.CognitoUser(userData)
}
export const getAuthDetails = (email, password) => {
const authenticationData = {
Username: email,
Password: password,
}
return new AmazonCognitoIdentity.AuthenticationDetails(authenticationData)
}
if you are using the backend api, one way is you can use adminGetUser method to get userStatus, more details here https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html

AWS Cognito User Pool check if there is a user with a specific Email

I want for my backend server (node.js) make a call through aws-sdk library to see if exists a user with specific mail. Is there a proper method to do this or a work arround without using user's credentials to do this procedure?
Yes this can be done the listUsers() AWS Javascript CognitoIdentityServiceProvider() API.
Call listUsers() to check if user exists
const cognito = new AWS.CognitoIdentityServiceProvider();
async function isEmailRegistered(email) {
//Check if user email is registered
var params = {
UserPoolId: 'eu-west-1_3bqeRjkSu', /* required */
AttributesToGet: [
'email',
],
Filter: "email = \"" + email + "\"",
};
return cognito.listUsers(params).promise();
}
Call and handle result
await isEmailRegistered(qsp.email).then( data => {
if (data.Users.length === 0) {
//User does not exist
} else {
//User does exist
}
}).catch(err => {
console.log("error: " + JSON.stringify(err))
});

Prevent users from signing up on their own with federated identity providers (FIP) but allow sign in with a FIP if added by an administrator

I've set up a user pool in Amazon Cognito for my web application. The application is not meant to be public and only specific users are allowed to sign in. The policies of that user pool in the Amazon Console allow only administrators to create new users.
I've implemented sign in through Facebook and Google. Cognito does indeed let users sign into the application with these federated identity providers, which is great. However, it seems that anybody with a Facebook or Google account can sign themselves up now.
So, on one hand, people can not create their own user with regular Cognito credentials but, on the other hand, they can create a new user in Cognito if they use a federated identity provider.
Is there a way to restrict signing into my application with Facebook or Google to only users that already exist in the user pool? That way, administrators would still be able to control who exactly can access the application. I would like to use the email shared by the federated identity provider to check if they are allowed to sign in.
The application is set up with CloudFront. I've written a Lambda that intercepts origin requests to check for tokens in cookies and authorize access based on the validity of the access token.
I would like to avoid writing additional code to prevent users to sign themselves up with Facebook or Google but if there is no other way, I'll update the Lambda.
So, here is the pre sign-up Lambda trigger I ended up writing. I took the time to use async/await instead of Promises. It works nicely, except that there is a documented bug where Cognito forces users who use external identity providers for the first time to sign up and then sign in again (so they see the auth page twice) before they can access the application. I have an idea on how to fix this but in the meantime the Lambda below does what I wanted. Also, it turns out that the ID that comes from Login With Amazon is not using the correct case, so I had to re-format that ID by hand, which is unfortunate. Makes me feel like the implementation of the triggers for Cognito is a bit buggy.
const PROVIDER_MAP = new Map([
['facebook', 'Facebook'],
['google', 'Google'],
['loginwithamazon', 'LoginWithAmazon'],
['signinwithapple', 'SignInWithApple']
]);
async function getFirstCognitoUserWithSameEmail(event) {
const { region, userPoolId, request } = event;
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({
region
});
const parameters = {
UserPoolId: userPoolId,
AttributesToGet: ['sub', 'email'], // We don't really need these attributes
Filter: `email = "${request.userAttributes.email}"` // Unfortunately, only one filter can be applied at once
};
const listUserQuery = await cognito.listUsers(parameters).promise();
if (!listUserQuery || !listUserQuery.Users) {
return { error: 'Could not get list of users.' };
}
const { Users: users } = listUserQuery;
const cognitoUsers = users.filter(
user => user.UserStatus !== 'EXTERNAL_PROVIDER' && user.Enabled
);
if (cognitoUsers.length === 0) {
console.log('No existing enabled Cognito user with same email address found.');
return {
error: 'User is not allowed to sign up.'
};
}
if (cognitoUsers.length > 1) {
cognitoUsers.sort((a, b) =>
a.UserCreateDate > b.UserCreateDate ? 1 : -1
);
}
console.log(
`Found ${cognitoUsers.length} enabled Cognito user(s) with same email address.`
);
return { user: cognitoUsers[0], error: null };
}
// Only external users get linked with Cognito users by design
async function linkExternalUserToCognitoUser(event, existingUsername) {
const { userName, region, userPoolId } = event;
const [
externalIdentityProviderName,
externalIdentityUserId
] = userName.split('_');
if (!externalIdentityProviderName || !externalIdentityUserId) {
console.error(
'Invalid identity provider name or external user ID. Should look like facebook_123456789.'
);
return { error: 'Invalid external user data.' };
}
const providerName = PROVIDER_MAP.get(externalIdentityProviderName);
let userId = externalIdentityUserId;
if (providerName === PROVIDER_MAP.get('loginwithamazon')) {
// Amazon IDs look like amzn1.account.ABC123DEF456
const [part1, part2, amazonId] = userId.split('.');
const upperCaseAmazonId = amazonId.toUpperCase();
userId = `${part1}.${part2}.${upperCaseAmazonId}`;
}
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({
region
});
console.log(`Linking ${userName} (ID: ${userId}).`);
const parameters = {
// Existing user in the user pool to be linked to the external identity provider user account.
DestinationUser: {
ProviderAttributeValue: existingUsername,
ProviderName: 'Cognito'
},
// An external identity provider account for a user who does not currently exist yet in the user pool.
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: userId,
ProviderName: providerName // Facebook, Google, Login with Amazon, Sign in with Apple
},
UserPoolId: userPoolId
};
// See https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html
await cognito.adminLinkProviderForUser(parameters).promise();
console.log('Successfully linked external identity to user.');
// TODO: Update the user created for the external identity and update the "email verified" flag to true. This should take care of the bug where users have to sign in twice when they sign up with an identity provider for the first time to access the website.
// Bug is documented here: https://forums.aws.amazon.com/thread.jspa?threadID=267154&start=25&tstart=0
return { error: null };
}
module.exports = async (event, context, callback) => {
// See event structure at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
const { triggerSource } = event;
switch (triggerSource) {
default: {
return callback(null, event);
}
case 'PreSignUp_ExternalProvider': {
try {
const {
user,
error: getUserError
} = await getFirstCognitoUserWithSameEmail(event);
if (getUserError) {
console.error(getUserError);
return callback(getUserError, null);
}
const {
error: linkUserError
} = await linkExternalUserToCognitoUser(event, user.Username);
if (linkUserError) {
console.error(linkUserError);
return callback(linkUserError, null);
}
return callback(null, event);
} catch (error) {
const errorMessage =
'An error occurred while signing up user from an external identity provider.';
console.error(errorMessage, error);
return callback(errorMessage, null);
}
}
}
};
There is a way to do this but you will need to write some code - there is no out-of-the-box solution.
You will need to write a lambda and connect it to the Cognito Pre-Signup trigger.
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html
The trigger has three different sources of event; PreSignUp_SignUp, PreSignUp_AdminCreateUser and PreSignUp_ExternalProvider.
Your lambda should check you have the PreSignUp_ExternalProvider event. For these events, use the Cognito SDK to look the user up in your existing pool. If the user exists, return the event. If the user does not exist, return a string (error message).
I will paste my own Pre-Signup trigger here. It does not do what you need it to, but all the main components you need are there. You can basically hack it into doing what you require.
const AWS = require("aws-sdk");
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = (event, context, callback) => {
function checkForExistingUsers(event, linkToExistingUser) {
console.log("Executing checkForExistingUsers");
var params = {
UserPoolId: event.userPoolId,
AttributesToGet: ['sub', 'email'],
Filter: "email = \"" + event.request.userAttributes.email + "\""
};
return new Promise((resolve, reject) =>
cognito.listUsers(params, (err, result) => {
if (err) {
reject(err);
return;
}
if (result && result.Users && result.Users[0] && result.Users[0].Username && linkToExistingUser) {
console.log("Found existing users: ", result.Users);
if (result.Users.length > 1){
result.Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate) ? 1 : -1);
console.log("Found more than one existing users. Ordered by createdDate: ", result.Users);
}
linkUser(result.Users[0].Username, event).then(result => {
resolve(result);
})
.catch(error => {
reject(err);
return;
});
} else {
resolve(result);
}
})
);
}
function linkUser(sub, event) {
console.log("Linking user accounts with target sub: " + sub + "and event: ", event);
//By default, assume the existing account is a Cognito username/password
var destinationProvider = "Cognito";
var destinationSub = sub;
//If the existing user is in fact an external user (Xero etc), override the the provider
if (sub.includes("_")) {
destinationProvider = sub.split("_")[0];
destinationSub = sub.split("_")[1];
}
var params = {
DestinationUser: {
ProviderAttributeValue: destinationSub,
ProviderName: destinationProvider
},
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: event.userName.split("_")[1],
ProviderName: event.userName.split("_")[0]
},
UserPoolId: event.userPoolId
};
console.log("Parameters for adminLinkProviderForUser: ", params);
return new Promise((resolve, reject) =>
cognito.adminLinkProviderForUser(params, (err, result) => {
if (err) {
console.log("Error encountered whilst linking users: ", err);
reject(err);
return;
}
console.log("Successfully linked users.");
resolve(result);
})
);
}
console.log(JSON.stringify(event));
if (event.triggerSource == "PreSignUp_SignUp" || event.triggerSource == "PreSignUp_AdminCreateUser") {
checkForExistingUsers(event, false).then(result => {
if (result != null && result.Users != null && result.Users[0] != null) {
console.log("Found at least one existing account with that email address: ", result);
console.log("Rejecting sign-up");
//prevent sign-up
callback("An external provider account alreadys exists for that email address", null);
} else {
//proceed with sign-up
callback(null, event);
}
})
.catch(error => {
console.log("Error checking for existing users: ", error);
//proceed with sign-up
callback(null, event);
});
}
if (event.triggerSource == "PreSignUp_ExternalProvider") {
checkForExistingUsers(event, true).then(result => {
console.log("Completed looking up users and linking them: ", result);
callback(null, event);
})
.catch(error => {
console.log("Error checking for existing users: ", error);
//proceed with sign-up
callback(null, event);
});
}
};

Unable to setup AWS-amplify MFA - issue with the Auth.confirmSignIn function

I'm having a problem with my adding MFA to my site which manages authentication with AWS Amplify.
I've followed the example here and created the code below to handle both users who have MFA enabled, and those that don't.
const Signin = props => {
const { classes, onStateChange, history } = props;
const [inputs, setInputs] = useState({
username: '',
password: '',
});
const [currentStep, setCurrentStep] = useState(1)
const [userObject, setUserObject] = useState({})
const [authCode, setauthCode] = useState({code: ''})
const onSignIn = async (e) => {
const username = inputs.username.toLowerCase()
await Auth.signIn({
username, // Required, the username
password: inputs.password, // Optional, the password
}).then(user => {
if (user.preferredMFA === 'SOFTWARE_TOKEN_MFA'){
setUserObject(user)
setCurrentStep(2)}
else{
onStateChange('signedIn', {})
history.push('/dashboard');
}
})
.catch(err => {
showAlert(err.message)
if (err.code === 'PasswordResetRequiredException') {
onStateChange('forgotPassword')
history.push('/forgotpassword');
}
})
}
const MfaSignIn = async (e) => {
const user=userObject
await Auth.confirmSignIn(
user,
authCode.code,
user.preferredMFA
).then(user => {
onStateChange('signedIn', {})
history.push('/dashboard');
})
.catch(err => {
showAlert(err.message)
if (err.code === 'PasswordResetRequiredException') {
onStateChange('forgotPassword')
history.push('/forgotpassword');
}
})
If MFA is not enabled the login page will call the onSignIn function then log them in. If MFA is enabled the user object returned from the signin function will return the value "SOFTWARE_TOKEN_MFA" for the preferredMFA key, and a second page will load which allows the user to enter their MFA code, which is handled by the MfaSignIn function.
No MFA works fine, however when attempting to use the MfaSignIn function as is I get "Missing required parameter Session"
the Session key of the user object returned by onSignIn has a value of null, and there's no mention of it needing to be added from the docs. Ive tried adjusting this function to
const MfaSignIn = async (e) => {
e.preventDefault(authCode.code);
const session= Auth.currentSession().then(data=> {return data})
const token = await session.then(data=>{return data.getAccessToken()})
const user=userObject
user.Session=token.jwtToken
await Auth.confirmSignIn(
user,
authCode.code,
user.preferredMFA
).then(user => {
onStateChange('signedIn', {})
history.push('/dashboard');
})
.catch(err => {
showAlert(err.message)
if (err.code === 'PasswordResetRequiredException') {
onStateChange('forgotPassword')
history.push('/upgrade');
}
})
}
Where I call the Auth.currentSession method and add the data from there to the Session value in the user object. Adding the whole object returns the error-
"Start of structure or map found where not expected."
- seems to need a string not a map object
I've tried adding each of the three JWT token strings that are found in the currentSession object, along with the result of the getAccessToken as in the example above. All return the error
"Invalid session provided"
I'm at a loss at what I need to give the Auth.confirmSignIn to let the user sign in- I appear to be doing everything correctly as per the docs.
Any ideas?

AWS Cognito node lambda migration user : authenticateUser is not defined

I would like to migrate users from userPool 1 to userPool 2 with the migration user lambda in AWS Console function. In order to do it, I have used the script provided by AWS but I can't find how I can use authenticateUser for instance. It is not defined when executed.
The migration lambda is executed.
authenticateUser is not defined
I have also tried to create a layer, imported succesfully and set the layer in my lambda function but cannot make it work too.
exports.handler = (event, context, callback) => {
var user;
if ( event.triggerSource == "UserMigration_Authentication" ) {
// authenticate the user with your existing user directory service
user = authenticateUser(event.userName, event.request.password);
if ( user ) {
event.response.userAttributes = {
"email": user.emailAddress,
"email_verified": "true"
};
event.response.finalUserStatus = "CONFIRMED";
event.response.messageAction = "SUPPRESS";
context.succeed(event);
}
else {
// Return error to Amazon Cognito
callback("Bad password");
}
}
else if ( event.triggerSource == "UserMigration_ForgotPassword" ) {
// Lookup the user in your existing user directory service
user = lookupUser(event.userName);
if ( user ) {
event.response.userAttributes = {
"email": user.emailAddress,
// required to enable password-reset code to be sent to user
"email_verified": "true"
};
event.response.messageAction = "SUPPRESS";
context.succeed(event);
}
else {
// Return error to Amazon Cognito
callback("Bad password");
}
}
else {
// Return error to Amazon Cognito
callback("Bad triggerSource " + event.triggerSource);
}
};
authenticateUser is not defined
My question is : how do we import this function ?
Thanks a lot.
That sample code is for migrating a user from a legacy database, and the authenticateUser, lookupUser functions are just abstractions for your business logic (which AWS can't write for you). For instance if you have to migrate from a legacy database (not a user pool), then you would lookup their user in your table, grab their salt, hash the password passed in to the migration trigger using the same logic you did in your legacy authentication method, compare it against the stored hashed password in your legacy database, etc. (It gets a little simpler if you were storing passwords in plaintext, but let's not consider that.)
Here's a snippet that should do most of the migration for you. Someone asked a similar question on Github and referenced this StackOverflow issue.
const AWS = require('aws-sdk');
const cognitoIdentity = new AWS.CognitoIdentityServiceProvider({ region: '<your-region-here>' });
const UserPoolId = process.env.deprecatedUserPoolId;
exports.handler = async (event) => {
const { userName } = event;
const getUserParams = {
Username: userName,
UserPoolId
};
try {
const user = await cognitoIdentity.adminGetUser(getUserParams).promise();
//TODO: if you have custom attributes, grab them from the user variable and store them in the response below
event.response = { finalUserStatus: "CONFIRMED" }
return event;
} catch (e) {
throw e; //no user to migrate, give them an error in the client
}
};