I am implementing a cloud function for updating the current user's password.
Basically, the logic I want to follow is:
(Client side)
0. Complete form and submit the data (current password and new password).
(Backend)
1. Get the current user email from the callable function context.
2. Re-authenticate the current user using the provided current password.
2.1. If success, change the password and send a notification email.
2.2. Else, throw an error.
Here is my current code:
const { auth, functions } = require("../../services/firebase");
...
exports.updatePassword = functions
.region("us-central1")
.runWith({ memory: "1GB", timeoutSeconds: 120 })
.https.onCall(async (data, context) => {
const { currentPassowrd, newPassword } = data;
const { email, uid: userId } = context.auth.token;
if (!userId) {
// throw ...
}
try {
//
// Problem: `firebase-admin` authentication doesn't include
// the `signInWithEmailAndPassword()` method...
//
await auth.signInWithEmailAndPassword(email, currentPassowrd);
await auth.updateUser(userId, {
password: newPassword,
});
sendPasswordUpdateEmail(email);
} catch (err) {
// ...
throw AuthErrors.cannotUpdatePassword();
}
});
My problem is that the firebase-admin package doesn't include the signInWithEmailAndPassword, and I need a way to handle this, to check that "currentPassword" is correct, inside my function.
My other option, if the one I have described is not possible, is to update the password using the firebase sdk in the client side, and then to call a firebase function to send the notification email.
Strictly speaking you don't need to re-authenticate the user in the Cloud Function: If you get a value for context.auth.uid in your Callable Cloud Function, it means that the user is authenticated in the front-end and you can therefore safely call the updateUser() method.
If you want to deal with the case when the user left his device opened, and someone updates his password, as explained in the comments under your question, I would suggest you use the reauthenticateWithCredential() method in the front-end, which re-authenticates a user using a fresh credential.
Do as follows:
import {
EmailAuthProvider,
getAuth,
reauthenticateWithCredential,
} from 'firebase/auth'
const email = auth.currentUser.email;
// Capture the password value
// e.g. via a pop-up window
const password = ...;
const auth = getAuth();
const credential = EmailAuthProvider.credential(
email,
password
);
await reauthenticateWithCredential(
auth.currentUser,
credential
);
// If no error is thrown, you can call the Callable Cloud Function, knowing the user has just re-signed-in.
Related
I want to implement role based authentication in my app.
After researching for a while I found a simple solution - to pass role to signIn function
signIn('email', { email, role: 'user' });
Then I can unpack it from req's body
import NextAuth from 'next-auth'
export default async function auth(req, res) {
return NextAuth(req, res, {
// providers, adapters, etc.
callbacks: {
signIn: async ({ user }) => {
const { body: { role } } = req;
if (role) {
user.role = role;
}
return true;
},
session: async ({ user, session }) => {
session.user = {...session.user, ...user };
return session
}
}
}
});
Or so I thought. Magic link sign in flow has two steps: sending email and confirming it.
The role I provide to signIn function is only available during the first step and saving user to users collection only happens after user confirms email, so when user confirms email there's no role.
I tried storing role in cookies
if (role) res.setHeader('Set-Cookie', 'user-role=' + role + ';Max-Age=600');
But cookies get overriden after I confirm email and the role is lost.
I don't want to create additional collections and/or records in my database.
How can I preserve role inside signIn callback without storing it in a separate database collection? Maybe there's some other solution you can think of?
The official snippet here says:
// You can access the new user via result.user
// Additional user info profile not available via:
// result.additionalUserInfo.profile == null
// You can check if the user is new or existing:
// result.additionalUserInfo.isNewUser
Even the API reference says :
Object that contains additional user information as a result of a
successful sign-in, link, or re-authentication operation.
However I get additionalUserInfo is undefined. I need to detect if the email link login is a new user or not.
My code:
await setPersistence(auth, browserLocalPersistence);
const result = await signInWithEmailLink(auth, email.value, window.location.href);
if (result && result.user) {
window.localStorage.removeItem('email');
window.localStorage.removeItem('its');
console.log(result.additionalUserInfo.isNewUser()); // undefined
return router.push({ path: "/dashboard" });
}
You need to use the getAdditionalUserInfo method separately in the new modular SDK as mentioned in this Github issue.
import {signInWithEmailLink, getAdditionalUserInfo} from "firebase/auth"
const result = await signInWithEmailLink(auth, email.value, window.location.href);
const {isNewUser} = getAdditionalUserInfo(result)
// Pass the UserCredential ^^^^^^
am working on a little project and i did finish all the authentication work but one thing,am wondering how to check if the email is real before going into the process of signup,
by the way am using react and Firebase and i did look online and i did find a package called email-existence i did try it and it dose return true if the email is real and false if the email dosent exist but thats not working when i use it with react it return an error
import firebase from '../util/firebase';
const emailExistence = require('email-existence');
export const normalSignup = (props, setSign, email, password, confirmPassword, username) => {
emailExistence.check(email, function (error, response) { // return error here addresses.sort is not a function
console.log('res: ' + response);
});
}
anyway am wondering if there's a way to do it with Firebase without external packages thanx in advance
PS:am not using cloud functions
Well assuming you want to check if the email is a verified email address you can write the code in the following way
import firebase from '../util/firebase';
const App = {
firebase: firebase,
getLoggedInUser: () => {
const currentUser = App.firebase.auth().currentUser
if (currentUser) {
return {
email: currentUser.email,
userId: currentUser.uid,
isEmailVerified: currentUser.emailVerified
}
} else {
return undefined
}
},
isAuthenticated: () => {
return (App.getLoggedInUser() && App.getLoggedInUser().isEmailVerified)
},
authenticate: async (email, password) => {
await App.firebase.auth().signInWithEmailAndPassword(email, password)
},
signup: async (email, password) => {
const userCredential = await App.firebase.auth().createUserWithEmailAndPassword(email, password)
await userCredential.user.sendEmailVerification()
return `Check your email for verification mail before logging in`
},
Here the following happens
When a user signs up the signup method is called and an email verification is sent by firebase as shown in the above code
When a user logs in the authenticate method is called so according to firebase you are logged in
However to redirect or render a certain page say after log in you can use the isAuthenticated method to display a page to a certain user
So you can pass method isAuthenticated as a prop to react-router and render your web application how you want.
This way only real and authentic email id which are verified will have access to your app
Note
This method is working already in prod but its using VueJS and is an opensource project on github let me know if you want to reference it
Maybe just use a regex to check if the email is valid?
According to this webpage for JavaScript you just need:
const emailRegex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (emailRegex.test(email)) {
console.log('Email valid!');
}
This won't stop people entering emails for incorrect domains, but ensures that if someone uses a mail server that isn't widely known, it will get accepted too.
Your only option on the client side (if you are on Firebase I suppose you don't have the luxury to run a Node backend) to fetch a similar service as email-existence which returns a "valid" or "invalid" response if you GET the endpoint with the email address.
These are usually premium services, but if you have low traffic you can try out a free one. In my example it is Mailboxlayer.
Their endpoint can be called like this (and of course if you are stick to the client side it means anyone can steal your api key from production via browser network tab!):
GET http://apilayer.net/api/check?access_key=YOUR_ACCESS_KEY&email=richard#example.com
Which returns a JSON:
{
"email": "richard#example.com",
"did_you_mean": "",
"user": "support",
"domain": "apilayer.net",
"format_valid": true,
"mx_found": true,
"smtp_check": true,
"catch_all": false,
"role": true,
"disposable": false,
"free": false,
"score": 0.8
}
Best to use score, which:
[...] returns a numeric score between 0 and 1 reflecting the quality and deliverability of the requested email address.
In React:
const [data, setData] = useState(null)
const [emailToVerify, setEmailToVerify] = useState('richard#example.com') // just for the sake of example
const apiKey = process.env.API_KEY
const fetchEmailVerificationApi = useCallback(async () => {
try {
const response = await fetch(`http://apilayer.net/api/check?access_key=${apiKey}&email=${emailToVerify}`)
const json = await response.json()
setData(json.score) // returns a numeric score between 0 and 1 reflecting the quality and deliverability of the requested email address.
} catch (e) {
console.error(e)
}
}, [apiKey, emailToVerify])
useEffect(() => {
fetchEmailVerificationApi()
}, [fetchEmailVerificationApi])
I am wondering how to make a document for each user as they create their account (with Firebase Web). I have Firebase Authentication enabled and working, and I'd like each user then to have a document in Cloud Firestore in a collection named users. How would I get the UID and then automatically create a document for each user? (I am doing this so that calendar events can be saved into an array field in the document, but I need a document for the user to start with). I am aware and know how to make security rules for access, I just don't know how to make the document in the first place.
Thanks!
While it is definitely possible to create a user profile document through Cloud Functions, as Renaud and guillefd suggest, also consider creating the document directly from your application code. The approach is fairly similar, e.g. if you're using email+password sign-in:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(function(user) {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
const email = user.email; // The email of the user.
const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
firebase.firestore().collection('accounts').doc(userUid).set(account);
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
Aside from running directly from the web app, this code also creates the document with the user's UID as the key, which makes subsequent lookups a bit simpler.
You´ll have to set a firebase function triggered by the onCreate() Auth trigger.
1. create the function trigger
2. get the user created data
3. set the account data.
4. add the account data to the collection.
functions/index.js
// Firebase function
exports.createAccountDocument = functions.auth.user().onCreate((user) => {
// get user data from the auth trigger
const userUid = user.uid; // The UID of the user.
//const email = user.email; // The email of the user.
//const displayName = user.displayName; // The display name of the user.
// set account doc
const account = {
useruid: userUid,
calendarEvents: []
}
// write new doc to collection
return admin.firestore().collection('accounts').add(account);
});
If you are using Firebase UI to simplify your life a lil, you can add a User document to a "/users" collection in Firestore only when that user first signs up by using authResult.additionalUserInfo.isNewUser from the signInSuccessWithAuthResult in your UI config.
I'm doing something like this in my project:
let uiConfig = {
...
callbacks: {
signInSuccessWithAuthResult: (authResult) => {
// this is a new user, add them to the firestore users collection!
if (authResult.additionalUserInfo.isNewUser) {
db.collection("users")
.doc(authResult.user.uid)
.set({
displayName: authResult.user.displayName,
photoURL: authResult.user.photoURL,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
})
.then(() => {
console.log("User document successfully written!");
})
.catch((error) => {
console.error("Error writing user document: ", error);
});
}
return false;
},
},
...
}
...
ui.start("#firebaseui-auth-container", uiConfig);
The signInSuccessWithAuthResult gives you an authResult and a redirectUrl.
from the Firebase UI Web Github README:
// ...
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
// If a user signed in with email link, ?showPromo=1234 can be obtained from
// window.location.href.
// ...
return false;
}
I am trying to change/update a user's email address using :
firebase.auth().changeEmail({oldEmail, newEmail, password}, cb)
But I am getting ...changeEmail is not a function error. I found the reference here from the old firebase docu.
So how to I do it in the 3.x version? Because I cant find a reference in the new documentation.
You're looking for the updateEmail() method on the firebase.User object: https://firebase.google.com/docs/reference/js/firebase.User#updateEmail
Since this is on the user object, your user will already have to be signed in. Hence it only requires the password.
Simple usage:
firebase.auth()
.signInWithEmailAndPassword('you#domain.example', 'correcthorsebatterystaple')
.then(function(userCredential) {
userCredential.user.updateEmail('newyou#domain.example')
})
If someone is looking for updating a user's email via Firebase Admin, it's documented over here and can be performed with:
admin.auth().updateUser(uid, {
email: "modifiedUser#example.com"
});
FOR FIREBASE V9 (modular) USERS:
The accepted answer will not apply to you. Instead, you can do this, i.e., import { updateEmail } and use it like any other import. The following code was copy/pasted directly from the fb docs at https://firebase.google.com/docs/auth/web/manage-users
Happy coding!
import { getAuth, updateEmail } from "firebase/auth";
const auth = getAuth();
updateEmail(auth.currentUser, "user#example.com").then(() => {
// Email updated!
// ...
}).catch((error) => {
// An error occurred
// ...
});
You can do this directly with AngularFire2, you just need to add "currentUser" to your path.
this.af.auth.currentUser.updateEmail(email)
.then(() => {
...
});
You will also need to reauthenticate the login prior to calling this as Firebase requires a fresh authentication to perform certain account functions such as deleting the account, changing the email or the password.
For the project I just implemented this on, I just included the login as part of the change password/email forms and then called "signInWithEmailAndPassword" just prior to the "updateEmail" call.
To update the password just do the following:
this.af.auth.currentUser.updatePassword(password)
.then(() => {
...
});
updateEmail needs to happen right after sign in due to email being a security sensitive info
Example for Kotlin
// need to sign user in immediately before updating the email
auth.signInWithEmailAndPassword("currentEmail","currentPassword")
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success now update email
auth.currentUser!!.updateEmail(newEmail)
.addOnCompleteListener{ task ->
if (task.isSuccessful) {
// email update completed
}else{
// email update failed
}
}
} else {
// sign in failed
}
}
async updateEmail() {
const auth = firebase.auth();
try {
const usercred = await auth.currentUser.updateEmail(this.email.value);
console.log('Email updated!!')
} catch(err) {
console.log(err)
}
}
You can use this to update email with Firebase.
Firebase v9:
const changeEmail = (userInput) => {
const { newEmail, pass } = userInput
signInWithEmailAndPassword(auth, oldEmail, pass)
.then(cred => updateEmail(cred.user, newEmail))
}