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))
});
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);
});
}
};
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
}
};
I am developing a custom auth flow in aws cognito with lambda triggers.
I used the sample code given by aws for lambda triggers DefineAuthChallenge_Authentication, CreateAuthChallenge_Authentication and VerifyAuthChallenge_Authentication, It's even explained in this answer,
AWS Cognito User Pool without a password
But when I sign in from frontend with amplify it says
{code: "InvalidLambdaResponseException", name: "InvalidLambdaResponseException", message: "Unrecognizable lambda output"}
After the Define auth challenge when create auth challenge is invoked the challenge is not in the event.request.session, it's an empty array. The challengeName attribute is just there in the event.response as following,
triggerSource: 'CreateAuthChallenge_Authentication',
request:
{ userAttributes:
{ sub: 'xxxxx-xxxxx-xxxx',
'cognito:user_status': 'CONFIRMED',
name: 'sala',
phone_number_verified: 'true',
'cognito:phone_number_alias': '+947xxxxxxxxx',
phone_number: '+947xxxxxxxxx' },
challengeName: 'CUSTOM_CHALLENGE',
session: [] },
Following is my define auth challenge,
`
exports.handler = async event => {
if (
event.request.session &&
event.request.session.length >= 3 &&
event.request.session.slice(-1)[0].challengeResult === false
) {
// The user provided a wrong answer 3 times; fail auth
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else if (
event.request.session &&
event.request.session.length &&
event.request.session.slice(-1)[0].challengeResult === true
) {
// The user provided the right answer; succeed auth
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// The user did not provide a correct answer yet; present challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
I have a file server that uses Cognito so users can access by authenticating themselves with basic authentication or the OAuth2.0 authorization code flow.
I'd like external apps to be able to authenticate themselves using the client credentials flow, and then be able to impersonate a user. Is there a way to do this with Cognito?
I was able to do this by creating custom lambdas for the Cognito triggers: Define Auth Challenge, Create Auth Challenge & Verify Auth Challenge.
My requirement was that I wanted my backend to use a secret to then get access & refresh tokens for any Cognito user.
Define Auth Challenge Lambda
exports.handler = async event => {
if (
event.request.session &&
event.request.session.length >= 3 &&
event.request.session.slice(-1)[0].challengeResult === false
) {
// The user provided a wrong answer 3 times; fail auth
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else if (
event.request.session &&
event.request.session.length &&
event.request.session.slice(-1)[0].challengeResult === true
) {
// The user provided the right answer; succeed auth
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// The user did not provide a correct answer yet; present challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
Create Auth Challenge Lambda
exports.handler = async event => {
if (event.request.challengeName == 'CUSTOM_CHALLENGE') {
// The value set for publicChallengeParameters is arbitrary for our
// purposes, but something must be set
event.response.publicChallengeParameters = { foo: 'bar' };
}
return event;
};
Verify Auth Challenge Lambda
exports.handler = async event => {
if (event.request.challengeName == 'CUSTOM_CHALLENGE') {
// The value set for publicChallengeParameters is arbitrary for our
// purposes, but something must be set
event.response.publicChallengeParameters = { foo: 'bar' };
}
return event;
};
I was then able to use some JS, using amazon-cognito-identity-js, to provide the secret and get the tokens:
var authenticationData = {
Username : 'username'
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId : '...', // Your user pool id here
ClientId : '...' // Your client id here
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username : 'username',
Pool : userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType('CUSTOM_AUTH');
cognitoUser.initiateAuth(authenticationDetails, {
onSuccess: function(result) {
// User authentication was successful
},
onFailure: function(err) {
// User authentication was not successful
},
customChallenge: function(challengeParameters) {
// User authentication depends on challenge response
var challengeResponses = 'secret'
cognitoUser.sendCustomChallengeAnswer(challengeResponses, this);
}
});