I am trying to setup a custom flow authentication with Cognito to enable Email MFA but when i attempt to login i get the error:
An error occurred (NotAuthorizedException) when calling the InitiateAuth operation: Incorrect username or password.
this is the api call i am using - i have doubled checked and tried using USER_PASSWORD_AUTH as the flow to make sure the users details are right and yes i can login when using this flow so they are correct.
aws cognito-idp initiate-auth --auth-flow CUSTOM_AUTH --auth-parameters USERNAME=testuser,PASSWORD=password1 --client-id clientId
Below is my define auth challenge
if (event.request.session.length == 1 && event.request.session[0].challengeName == 'SRP_A') {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'PASSWORD_VERIFIER';
} else if (event.request.session.length == 2 && event.request.session[1].challengeName == 'PASSWORD_VERIFIER' && event.request.session[1].challengeResult == true) {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (event.request.session.length == 3 && event.request.session[2].challengeName == 'CUSTOM_CHALLENGE' && event.request.session[2].challengeResult == true) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
context.done(null, event);
}
Is there something wrong with this? This code is copied from the aws cognito custom flow guide for define auth so i am struggling to know what is wrong.
I think I know what's wrong, and it's not the define-auth-challenge trigger.
In create-auth-challenge, if you specify a event.response.clientMetadata, you MUST specify the private or public challengeParameters, or both, otherwise it will not work.
i.e.
create-auth-challenge.js
event.response.challengeMetadata = "TOKEN_CHECK";
// 400 Incorrect username or password.
create-auth-challenge.js
event.response.challengeMetadata = "TOKEN_CHECK";
event.response.privateChallengeParameters = {
test: "test"
};
event.response.publicChallengeParameters = {
test: "test"
};
// 200 Ok
It's nasty because the error message is misleading.
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 am attempting to set up passwordless sms authentication using amplify in an IOS project. I have lambda triggers taken from a github repo set up this way:
CreateAuthChallenge
// ### About this Flow ###
// Using Custom Auth Flow through Amazon Cognito User Pools with Lambda Triggers to complete a 'CUSTOM_CHALLENGE'.
//
// ### About this function ###
// This CreateAuthChallengeSMS function (2nd of 4 triggers) creates the type of 'CUSTOM_CHALLENGE' as a one-time pass code sent via SMS. A one-time randomly generated 6-digit code (passCode)
// is sent via SMS (through Amazon SNS) to the user's mobile phone number during authentication. The generated passCode is stored in privateChallengeParameters.passCode and passed to the VerifyAuthChallenge function
// that will verify the user's entered passCode (received via SMS) into the mobile/web app matches the passCode passed privately through privateChallengeParameters.passCode.
// ### Next steps ###
// Instead of using the "crypto-secure-random-digit" library to generate random 6-digit codes, create a base32 secret for the user (if not exist) and
// generate a 6-digit code based on this secret. Much like TOTP except for the secret is never shared with the user. With a base32 secret associated with the user,
// we can easily switch from 6-digit code via SMS to 6-digit code generated based on shared secret via TOTP using the OATH module of a YubiKey or an authenticator app.
//
// Updated: Jan 6, 2020
'use strict';
const crypto_secure_random_digit = require("crypto-secure-random-digit");
const AWS = require("aws-sdk");
var sns = new AWS.SNS();
// Main handler
exports.handler = async (event = {}, context, callback) => {
console.log('RECEIVED event: ', JSON.stringify(event, null, 2));
let passCode;
var phoneNumber = event.request.userAttributes.phone_number;
// The first CUSTOM_CHALLENGE request for authentication from
// iOS AWSMobileClient actually comes in as an "SRP_A" challenge (a bug in the AWS SDK for iOS?)
// web (Angular) comes in with an empty event.request.session
if (event.request.session && event.request.session.length && event.request.session.slice(-1)[0].challengeName == "SRP_A" || event.request.session.length == 0) {
passCode = crypto_secure_random_digit.randomDigits(6).join('');
await sendSMSviaSNS(phoneNumber, passCode);
} else {
const previousChallenge = event.request.session.slice(-1)[0];
passCode = previousChallenge.challengeMetadata.match(/CODE-(\d*)/)[1];
}
event.response.publicChallengeParameters = { phone: event.request.userAttributes.phone_number };
event.response.privateChallengeParameters = { passCode };
event.response.challengeMetadata = `CODE-${passCode}`;
console.log('RETURNED event: ', JSON.stringify(event, null, 2));
callback(null, event)
};
// Send secret code over SMS via Amazon Simple Notification Service (SNS)
async function sendSMSviaSNS(phoneNumber, passCode) {
const params = { "Message": "[Mapwork] Your secret code: " + passCode, "PhoneNumber": phoneNumber };
await sns.publish(params).promise();
}
DefineAuthChallenge
// ### About this Flow ###
// Using Custom Auth Flow through Amazon Cognito User Pools with Lambda Triggers to complete a 'CUSTOM_CHALLENGE'. This is the same flow as one-time passcode generated and sent via SMS or Email.
// Instead, the service and user share a secret that was created during registration and both generate a 6-digit code based on the shared secret.
// If the two codes (typically only good for 30 seconds) match, the user is authenticated.
//
// ### About this function ###
// This DefineAuthChallengeCustom function (1st and 4th of 4 triggers) defines the type of challenge-response required for authentication.
// For HOTP, TOTP, U2F, or WebAuthn flows, we'll always use 'CUSTOM_CHALLENGE' and this function code won't change between the various auth methods.
// ### Next steps ###
// Updated: June 12, 2020
'use strict';
exports.handler = async (event) => {
console.log('RECEIVED event: ', JSON.stringify(event, null, 2));
// The first auth request for CUSTOM_CHALLENGE from the AWSMobileClient (in iOS native app) actually comes in as an "SRP_A" challenge (BUG in AWS iOS SDK), so switch to CUSTOM_CHALLENGE and clear session.
if (event.request.session && event.request.session.length && event.request.session.slice(-1)[0].challengeName == "SRP_A") {
console.log('New CUSTOM_CHALLENGE', JSON.stringify(event, null, 2));
event.request.session = [];
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
// User successfully answered the challenge, succeed with auth and issue OpenID tokens
else if (event.request.session &&
event.request.session.length &&
event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' &&
event.request.session.slice(-1)[0].challengeResult === true) {
console.log('The user provided the right answer to the challenge; succeed auth');
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
// After 3 failed challenge responses from user, fail authentication
else if (event.request.session && event.request.session.length >= 4 && event.request.session.slice(-1)[0].challengeResult === false) {
console.log('FAILED Authentication: The user provided a wrong answer 3 times');
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
// The user did not provide a correct answer yet; present CUSTOM_CHALLENGE again
else {
console.log('User response incorrect: Attempt [' + event.request.session.length + ']');
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
console.log('RETURNED event: ', JSON.stringify(event, null, 2));
return event;
};
VerifyAuthChallenge
// ### About this Flow ###
// Using Custom Auth Flow through Amazon Cognito User Pools with Lambda Triggers to complete a 'CUSTOM_CHALLENGE'. This custom challenge is using
// a randomly generated 6-digit sent to the user via SMS using Amazon SNS.
//
// ### About this function ###
// This VerifyAuthChallengeSMS function (3rd of 4 triggers) takes the user's 6-digit code sent via event.request.challengeAnswer parameter
// and returns TRUE if the user's passCode matches event.request.privateChallengeParameters.passCode.
// ### Next steps ###
'use strict';
exports.handler = async (event) => {
console.log('RECEIVED Event: ', JSON.stringify(event, null, 2));
let expectedAnswer = event.request.privateChallengeParameters.passCode || null;
if (event.request.challengeAnswer === expectedAnswer) {
event.response.answerCorrect = true;
}
else {
event.response.answerCorrect = false;
}
console.log('RETURNED Event: ', JSON.stringify(event, null, 2));
return event;
};
And then I invoke Auth.signIn() from the ios mobile device. However,I receive this error when I do so:
Sign in failed AuthError: Invalid lambda function output : Invalid JSON
I have been struggling for hours with this one and any help would be appreciated.
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);
}
});
As Auth Challenge for my User Pool I defined a Lambda Function. This function sends a request to Authy to requiere One Touch Authentication.
I would like to have this setup to add Authy Multi-Factor Authentication to the Cognito Login Process.
However when I authenticate, login a Cognito User with username and passwort this lambda function is not triggered!
What do I wrong? Are the Lambda Triggers only for defined for the Register Process? Thanks
---------------------Update:------------------------------------------------
My login code, that requires username and password:
authenticate(userName, userPassword) {
var userData = {Username: userName, Pool : CognitoUserPool}
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
var authenticationData = {Username : userName, Password : userPassword};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (session) {
AWSInitialize(cognitoUser, session);
face.showHome();
}.bind(this),
mfaRequired: function(session){
new MFAConfirmation(cognitoUser, 'login');
},
onFailure: function(err) {
alert(err);
}
});
};
Where the Trigger is defined:
The Define Auth Challenge lambda works only in the context of the CUSTOM_AUTH flow so you would have to pass CUSTOM_AUTH as the AuthFlow when authenticating. In Javascript you can do:
cognitoUser.setAuthenticationFlowType('CUSTOM_AUTH');
The examples in the Cognito Working with AWS Lambda Triggers developer guide actually do that. They let you define another challenge after authenticating with username and password.
The Define Auth Challenge example lets you setup another challenge after authenticating with username and password. It invokes the Create Auth Challenge lambda trigger when you specify CUSTOM_CHALLENGE as the challenge.
exports.handler = function(event, context) {
if (event.request.session.length == 1 && event.request.session[0].challengeName == 'SRP_A') {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'PASSWORD_VERIFIER';
} else if (event.request.session.length == 2 && event.request.session[1].challengeName == 'PASSWORD_VERIFIER' && event.request.session[1].challengeResult == true) {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (event.request.session.length == 3 && event.request.session[2].challengeName == 'CUSTOM_CHALLENGE' && event.request.session[2].challengeResult == true) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
context.done(null, event);
}