I have an issue I've been unable to resolve. I'm using AWS Cognito User Pools with a user migration lambda function to import users on the fly. After successfully validating a user's password, I'd like to check if this password meets the requirements Cognito enforces. As per the docs, this should be done in code:
Amazon Cognito doesn't enforce the password strength policy that you configured for the user pool during migration using Lambda trigger. If the password doesn't meet the password policy that you configured, Amazon Cognito still accepts the password so that it can continue to migrate the user. To enforce password strength policy and reject passwords that don't meet the policy, validate the password strength in your code. Then, if the password doesn't meet the policy, set finalUserStatus to RESET_REQUIRED.
However, when I set finalUserStatus to RESET_REQUIRED, I see behavior I don't understand. When logging in for the first time, I see the following, which seems correct:
At this time, I also receive an email with Subject: "Your temporary password" and body: "Your username is ...#gmail.com and temporary password is l6*NWOEp.". I thought this was odd because I hadn't submitted the above form yet. When I enter my email in the above form, I receive:
I don't receive any additional emails. When I fill out this form, I receive the same error.
When I go back to the sign in page and use the temporary password I received in the email, I am forwarded to the first form above: the one with the "Password reset required for user due to security reasons".
Below is the code, I've removed unrelated config and error handling:
export const lambdaHandler = async (event: any, context: any): Promise<any> => {
if (event.triggerSource == "UserMigration_Authentication") {
const userName = event.userName;
const password = event.request.password;
const user = await authenticateUser(userName, password);
event.response.userAttributes = {
"email": user.email,
"username": user.email,
"email_verified": "true",
};
if (passwordIsWeak(password) { // always true for testing purposes
event.response.finalUserStatus = "RESET_REQUIRED";
}
context.succeed(event);
}
};
The user in my Cognito User Pool has a Confirmation Status of "Reset required".
I've tried many combinations of finalUserStatus (RESET_REQUIRED, CONFIRMED, FORCE_CHANGE_PASSWORD) and messageAction (SUPPRESS, RESEND) returned in event.response. Anyway, thanks for any help provided!
Related
I am designing 2 pages for a user signing up. The first page is where the user enters their email only. I then perform this code.
await createUserWithEmailAndPassword(auth, email.value, bcrypt.hashSync(email.value, 6))
.then(async (userCredential) => {
sendEmailVerification(userCredential.user)
loading = false
navigate('/verifyEmail')
})
.catch((error) => {
log.error(`Error registering user: ${error}`)
errorMessage = error.message
isEmailInvalid = true
loading = false
})
This sends the user a verification email, which they then click on to set their password and name:
let oobCode = ''
oobCode = window.location.href.split('oobCode=')[1].split('&')[0]
const email = window.location.href.split('email=')[1].split('/')[0]
let user = auth.currentUser
await applyActionCode(auth, oobCode)
.then(async (result) => {
console.log(auth.currentUser)
Promise.all([
updateProfile(auth.currentUser, {displayName: firstName + ' ' + lastName}),
updatePassword(auth.currentUser, password),
])
console.log('Welcome', firstName, lastName)
await setUser('local', undefined, auth.currentUser)
})
However at this point, auth.currentUser will be null if the user has clicked on this link on a different browser. A quick workaround would be to create a user with password 'password' and then sign them in after applying the action code. However this has a big security flaw obviously, my current idea just encrypts their email as a temporary password in hopes they cannot guess it and sign in.
I guess my question is, how do I update the user upon applying an action code? If I can't do this what flow of operations should I change?
My question is, how do I update the user upon applying an action code?
If the user is using another browser it is not possible since he/she does not know the password.
If I can't do this what flow of operations should I change?
IMHO you are overcomplexifying the onboarding process. The common approach is to create the account with the password being chosen by the user (which, in your case, should happen in the first screen) and send the email for verification.
In parallel you deny access to the Firebase back-ends (DBs, Cloud Storage, etc.) to users with non verified email via the Security Rules.
Upon email verification you sign in the user:
If it is from the same browser the users is actually already signed in (side effect of the use of createUserWithEmailAndPassword()
If it is in a different browser the user just has to enter his email and password (which he/she knows)
You can use updateEmail with the following workflow:
await user.updateEmail(newEmail)
await user.getIdToken()
await user.sendEmailVerification()
and there's also a function verifyBeforeUpdateEmail.
These two workflows seems identical to me, but is there any difference?
The documentation lacks examples and explanations of the difference.
Reference: https://firebase.google.com/docs/reference/js/firebase.User#verifybeforeupdateemail
The User.verifyBeforeUpdateEmail method is documented as:
Sends a verification email to a new email address. The user's email will be updated to the new one after being verified.
So the process here sends a verification email to the new email address. Only once the user clicks the link in that email will their email address be updated and the emailVerified property of their account set to true.
The user.updateEmail method is documented as:
Updates the user's email address.
So when you use updateEmail, the user's email address ends up being unverified. If you care about email verification, you'll need to call sendEmailVerification again to verify the updates email address. But even if you call sendEmailVerification right after updating the email address, the user account will have its emailVerified property set fo false for a while.
I want to add to my web app that after order I'm sending a mail.
I choose Nodemailer because it's the most famous npm to use.
I coded my request and in the local environment, it's working.
I uploaded the code to Heroku and I get an Error.
Error: Invalid login: 534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbs
I checked people told me to disable the captcha wish I did here: UnlockCaptcha
And now I still get the same error, and I get a mail that google blocked the connection what can I do?
const nodemailer = require('nodemailer');
const { sendLog } = require('../middleware/sendLog');
const { coupons, actions } = require('../constant/actionCoupon');
var simple = function () {
var textMultiple = {
text1: 'text1',
text2: 'text2',
};
return textMultiple;
};
// send mail system for the (REQUEST ACCEPTED SYSTEM)
const sendMail = (mail, action) => {
let mailTransporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.MAIL,
pass: process.env.PASSWORD,
},
});
let mailDetails = {
from: process.env.MAIL,
to: mail,
subject: `Thank you for your purchase. with love FameGoal`,
text: "for any probleme please reply on this message",
};
mailTransporter.sendMail(mailDetails, function (err, data) {
if (err) {
console.log(err);
console.log(`error sent mail to ${mail}`, 'error');
} else {
console.log('succeed');
console.log(`succesfully sent mail to ${mail}`, 'info');
}
});
};
exports.sendMail = sendMail;
Using Gmail as an SMTP relay isn't the most ideal because Google servers may reject basic username/password authentication at times.
There are some workarounds. The most ideal is to use OAuth2 to send emails.
OAuth2
OAuth2 uses access tokens to perform authentication instead of a password.
I won't go over the steps to set up OAuth2 because it can take some time but if you're interested, this answer: https://stackoverflow.com/a/51933602/10237430 goes over all of the steps.
App passwords
If the Google account you're trying to send emails from has two step verification enabled, using a password to send emails will not work. You instead need to generate a app-specific password on Google's site and pass that in the password field.
More info on that here: https://support.google.com/accounts/answer/185833?hl=en
Enabling less secure apps
If you still want to use your current setup, you have to make sure you enable less secure apps on the Google account you're sending emails from. This will let you authenticate with Google using just an email and a password.
More info on that here: https://support.google.com/accounts/answer/6010255?hl=en
Basic password authentication will not work until you enable less secure apps.
Action:
- signInWithPhoneNumber(NUMBER NOT IN DB, recaptchaVerifier)
Expected Behavior:
- Since number not in DB, it should not log me in.
Current Behavior:
- If the number does not exist in DB, it CREATES a new user after going through recaptcha + sms verification. WHY?
Code:
function loginWithSMS(phoneNumber) {
firebase.auth().useDeviceLanguage();
//#ts-ignore
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier("recaptcha-container");
//#ts-ignore
window.recaptchaVerifier.render().then(function (widgetId) {
//#ts-ignore
window.recaptchaWidgetId = widgetId;
});
// #ts-ignore
firebase
.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier)
.then((confirmationResult) => {
console.log("Login success", confirmationResult);
window.recaptchaVerifier.clear();
// SMS sent. Prompt user to type the code from the message, then sign the
// user in with confirmationResult.confirm(code).
const verificationCode = window.prompt(
"Please enter the verification " + "code that was sent to your mobile device."
);
return confirmationResult.confirm(verificationCode);
})
.catch((error) => {
console.error(error);
// Error; SMS not sent
// Handle Errors Here
window.recaptchaVerifier.clear();
return Promise.reject(error);
});
}
This is just how the API is defined: by sending a text to the number, Firebase allows the user to verify that they have access to that phone number. If they do, they're allowed to sign in.
This is the same for the email+password provider in Firebase Authentication. Calling firebase.auth().createUserWithEmailAndPassword(email, password) creates the user, even if they didn't exist yet. And while your code may not call this API, any developer can take the Firebase configuration data from your app and call the API themselves.
Most often when developers are asking about this they're confusing authentication with authorization.
When you authenticate, you are proving that you are you. So in the examples above, that you have access to a certain phone number, or that you know the email+password combination of the account.
Based on knowing who the user is, the application then authorizes that user to perform certain actions or to access certain data.
For example, if you're using Realtime Database, Cloud Storage, or Cloud Firestore, you can control access with Firebase's server-side security rules.
If you have a different back-end, you'd control it there by checking the information in the ID token of the user (which you get from Firebase Authentication) against some set of authorization rules for your application.
Also see:
Prevent user account creation with sign in by email in firestore (similar question, but then for passwordless email signin)
How to disable Signup in Firebase 3.x
How does the firebase authentication and realtime application database secure itself?
My solution works, but I'm not sure this is safe and appropriate. On the front end I have a ReactJS app that send with axios a request with the login and password. On the back end I have NodeJS + ExpressJS handling the request as follows:
router.post('/', function(req, res, next) {
// get the records that match the login provided
const sql = "SELECT name, surname, login, password, blocked FROM users WHERE login=?";
query(sql, [req.body.login])
.then((result)=> {
// if there are 1 or more results, compare the passwords with bcrypt
if (result.length > 0) {
bcrypt.compare(req.body.password, result[0].password, function(err, success) {
if (success) {
// if the user is not blocked, send the status 200 with user's data
result[0].blocked ?
res.status(401).json({type: 'Warning', message: 'Your account has been blocked. Plase contact the admins.'})
:
res.status(200).json({name: result[0].name, surname: result[0].surname, email: result[0].email});
} else {
// send an error if the password is wrong
res.status(401).json({type: 'Error', message: 'Please check that your login and password are correct.'});
}
});
} else {
// send an error if the login was not found
res.status(401).json({type: 'Error', message: 'Please check that your login and password are correct.'});
}
});
});
Is it enough/safe to query the db for the provided login (it's unique) with if (result.length > 0)?
Is it ok to have the error message contained in the server response like this?
res.status(401).json({type: 'Warning', message: 'Your account has been blocked. Plase contact the admins.'})
I have the chance to let the user know if he typed the correct login but the wrong password; should I let him know that? I think it would give to malicious users the knowledge that the login actually exists, so for now I just send a generic login/pwd error. Is this ok?
Is ok to send the user's data from the server to the client if the login was successful?
Is it ok to have the error message contained in the server response like this?
I have the chance to let the user know if he typed the correct login but the wrong password; should I let him know that? I think it would give to malicious users the knowledge that the login actually exists, so for now I just send a generic login/pwd error. Is this ok?
Your implementation is good enough. It's also a good practice letting users know why they are unable to login without giving out too much information EVEN when it's a problem with their supplied credentials (something you are doing already).
Is it enough/safe to query the db for the provided login (it's unique) with if (result.length > 0)?
Yes, this is fine too. You may also want to add a LIMIT 1 to your query to give you a little performance boost since there is no point having your DB scan through all the records when you expect only one result.
It is also a good practice to only send the minimum amount of information and request for more on demand.
As a general observation of your code, you would benefit from the following:
Doing some error checking on your request object before querying the database at all (good practice too) as there is no guarantee that a valid or well formatted username/password would be sent with the request.
Moving the responses into another file to make your code cleaner and maintainable.