I have my own custom server that also records users. So what I want to achieve is, after a user registers, Then I will generate a custom token by signInWithCustomToken, then send a sendEmailVerification to them for verifying.
Here is how I generate the custom token from my backend
FirebaseAuth.getInstance().createCustomToken(id, additionalClaims)
Then in my front end, it will signInWithCustomToken right after they registered. (this is to get the user for sendEmailVerification)
await axios
.post(`/authorization/sign-up`, {
username: inputData.username,
password: inputData.password,
})
.then(async (response) => {
const auth = getAuth();
signInWithCustomToken(auth, response.data.content.token)
.then(async(userCredential) => {
await updateEmail(userCredential.user, inputData.username)
const actionCodeSettings = {
url: `https://domain host/test?email=${inputData.username}`
};
await sendEmailVerification(userCredential.user, actionCodeSettings)
navigate("/"); //login page
})
})
.catch((error) => toast(error.message));
} else {
toast("Please input valid data");
}
In my Login Backend I will check my database's user, whether their email has been verified or not. So if its not verified, the login will be rejected
select * from user where user = foo and password = foo and isEmailVerified = true
In my front-end verify-email page :
console.log("verifying email")
const mode = searchParams.get('mode') // mode = verifyEmail
const oobCode = searchParams.get('oobCode')
const auth = getAuth()
applyActionCode(auth, oobCode).then(() => {
//how to update my backend based on the user's email
})
Then in my Backend I will check the decodedToken.isEmailVerified, if its false, I will force them to activate their email first.
So, after the user click the activation link, it will be redirected to my verify-email page, which then will also update the status of the user in my database. But, I notice that the applyActionCode function returns Promise<void>.
For example if I register my email through my PC/Laptop, then I confirm the verify email through my phone / table / other device, then I won't be able to get the currentUser to retrieve some data, ex. their email.
I also cannot put the email into the continueUrl parameter right? since the user might change the parameter themself.
So How can I get the user's email safely after applyActionCode?
Or do I just redirect them back to the login page, then each time after they click login, I will compare the decodedToken.isEmailVerified vs my database's isEmailVerified ?
Related
I am new in Reactjs and I am working on "login" module, I integrated the login module successfully, and Now I want if the user enters the correct email and password then the email should be in session, And if the user is not logged in he can't see any further page, How can I do this? Here is my current code
const data = {
email: email,
password: password
};
axios.post("https://xxxxxxxxxxx/Checklogin/", data).then(function (response) {
if (response.data.msg == "wrong") {
$("#errormsg4").show("slow").delay(5000).fadeOut();
} else {
//"response.data.email"should be in session
}
});
To store the email in a session in React, as you specified, you can use the sessionStorage API. The sessionStorage API allows you to store key-value pairs in the user's browser. The data is stored in the session, which means it will only be available for the current tab and will be deleted when the user closes the tab.
Here is an example of how you can use the sessionStorage API to store the email in a session:
const data = {
email: email,
password: password
};
axios.post("https://xxxxxxxxxxx/Checklogin/", data).then(function (response) {
if (response.data.msg == "wrong") {
$("#errormsg4").show("slow").delay(5000).fadeOut();
} else {
// Store the email in the session
sessionStorage.setItem("email", response.data.email);
}
});
To check if the user is logged in, you can use the sessionStorage.getItem() method to retrieve the email from the session. If the getItem() method returns a non-null value, it means that the user is logged in and has an email stored in the session.
Here is an example of how you can use the sessionStorage.getItem() method to check if the user is logged in:
const email = sessionStorage.getItem("email");
if (email) {
// The user is logged in
} else {
// The user is not logged in
}
I hope this helps!
I created an app that supports both Email/Password and Google authentication. I found that if I created an account in a first way, but logged out and in again with Google, the origin password was gone, and no way to sign in with email anymore. Is there any way to avoid so?
// Google authentication
const signInWithGoogle = useCallback(
async event => {
event.preventDefault();
const provider = new firebase.auth.GoogleAuthProvider();
try {
await firebaseApp
.auth()
.signInWithRedirect(provider)
.then(function(result) {
var user = result.user.providerId;
alert(user);
});
history.push("/transfer");
} catch(error) {
alert(error.message);
}
},
[history]
);
//Email/Password sign-in
const handleLogin = useCallback(
async event => {
event.preventDefault();
const { email, password } = event.target.elements;
try {
await firebaseApp
.auth()
.signInWithEmailAndPassword(email.value, password.value)
.then(function(result) {
var user = result.user.providerId;
alert(user);
});
history.push("/transfer");
} catch (error) {
alert(error);
}
},
[history]
);
// Email/Password sign-up
const handleSignUp = useCallback(async event => {
event.preventDefault();
const { email, password } = event.target.elements;
try {
await firebaseApp
.auth()
.createUserWithEmailAndPassword(email.value, password.value);
history.push("/usersignupcred");
} catch (error) {
alert(error);
}
}, [history]);
Here in the documentation you can see this explanation:
Note that some providers, such as Google and Microsoft, serve as both email and social identity providers. Email providers are considered authoritative for all addresses related to their hosted email domain. This means a user logging in with an email address hosted by the same provider will never raise this error (for example, signing in with Google using an #gmail.com email, or Microsoft using an #live.com or #outlook.com email).
I would recommend to use as similar approach like here from the docu:
// User tries to sign in with Facebook.
auth.signInWithPopup(new firebase.auth.FacebookAuthProvider()).catch(err => {
// User's email already exists.
if (err.code === 'auth/account-exists-with-different-credential') {
// The pending Facebook credential.
var pendingCred = err.credential;
// The provider account's email address.
var email = err.email;
// Get the sign-in methods for this email.
auth.fetchSignInMethodsForEmail(email).then(methods => {
// If the user has several sign-in methods, the first method
// in the list will be the "recommended" method to use.
if (methods[0] === 'password') {
// TODO: Ask the user for their password.
// In real scenario, you should handle this asynchronously.
var password = promptUserForPassword();
auth.signInWithEmailAndPassword(email, password).then(result => {
return result.user.linkWithCredential(pendingCred);
}).then(() => {
// Facebook account successfully linked to the existing user.
goToApp();
});
return;
}
// All other cases are external providers.
// Construct provider object for that provider.
// TODO: Implement getProviderForProviderId.
var provider = getProviderForProviderId(methods[0]);
// At this point, you should let the user know that they already have an
// account with a different provider, and validate they want to sign in
// with the new provider.
// Note: Browsers usually block popups triggered asynchronously, so in
// real app, you should ask the user to click on a "Continue" button
// that will trigger signInWithPopup().
auth.signInWithPopup(provider).then(result => {
// Note: Identity Platform doesn't control the provider's sign-in
// flow, so it's possible for the user to sign in with an account
// with a different email from the first one.
// Link the Facebook credential. We have access to the pending
// credential, so we can directly call the link method.
result.user.linkWithCredential(pendingCred).then(usercred => {
// Success.
goToApp();
});
});
});
}
});
But instead of waiting for the error to be raised (none will be raised if using Google login as you also explained in your case) try always to call first fetchSignInMethodsForEmail and if the user has the email provider and tries now to use the Google one first log him in with the email provider and link him later with the Google provider.
This is the code that i'm practicing in to create a new user. I can receive the email verification and confirm it however, the site will still logged me in even if I have not yet confirmed my email yet.
try{
const { user } = await auth.createUserWithEmailAndPassword(email,password);
await user.sendEmailVerification();
await handleUserProfile(user, { displayName});
this.setState({
...initialSate
});
}catch(err){
console.log(err);
}
}
This is the handleUserProfile in another js file.
export const handleUserProfile = async (userAuth, additionalData) => {
if (!userAuth) return;
const {uid} = userAuth;
const userRef = firestore.doc(`users/${uid}`);
//create new user
const snapshot = await userRef.get();
if (!snapshot.exists){
const { displayName, email} = userAuth;
const timestamp = new Date();
//if the user exist does not exist
try{
await userRef.set({
displayName,
email,
createdDate: timestamp,
...additionalData
});
}catch(err){
console.log(err);
}
}
return userRef;
};
Everything is explained in the firebase documentation.
There you have the corresponding code snippets to try.
You would need to narrow down your question with some of this trials.
Even you have the chance to check if user opens the link from a differenc device from which waas signed up.
I think this is the snippet you might need:
// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
// Additional state parameters can also be passed via URL.
// This can be used to continue the user's intended action before triggering
// the sign-in operation.
// Get the email if available. This should be available if the user completes
// the flow on the same device where they started it.
var email = window.localStorage.getItem('emailForSignIn');
if (!email) {
// User opened the link on a different device. To prevent session fixation
// attacks, ask the user to provide the associated email again. For example:
email = window.prompt('Please provide your email for confirmation');
}
// The client SDK will parse the code from the link for you.
firebase.auth().signInWithEmailLink(email, window.location.href)
.then((result) => {
// Clear email from storage.
window.localStorage.removeItem('emailForSignIn');
// 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
})
.catch((error) => {
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
}
The site will still logged me in even if I have not yet confirmed my
email yet.
Yes this is how it is implemented in Firebase: there is nothing, out of the box, that prevents a user with a non-verified email to authenticate to your app.
You should manage that yourself, by:
Checking the email is verified in the back-end security rules (Firestore, Cloud Storage, etc..). For example with a function like:
function isVerifiedEmailUser() {
return request.auth.token.email_verified == true;
}
Possibly redirect and logout the user from your app if his/her email is not verified. For example, right after signing-up, as follows:
try {
const { user } = await auth.createUserWithEmailAndPassword(email,password);
await user.sendEmailVerification();
if (user.emailVerified) {
// display the content, redirect to another page, etc...
} else {
auth.signOut(); // Maybe call that after showing an error message
}
} catch(err){
console.log(err);
}
}
plus, potentially, something similar with signInWithEmailAndPassword() and onAuthStateChanged().
I am creating a form, in react-redux to change user password. I am wondering how can I validate the user current password in order to change to new one.
in my form I have 2 fields: old password, new password.
this is my action:
const { currentUser } = auth
currentUser.updatePassword(newPassword)
.then(
success => {
dispatch({
type: CHANGE_USER_PASSWORD_SUCCESS,
payload: currentUser
})
},
error => {
dispatch({
type: CHANGE_USER_PASSWORD_FAIL,
error: error.message
})
}
)
I am wondering, how to validate the old password in firebase? Should I use signInWithEmailAndPassword()? Or, is there a function to validate the current password without calling the signIn again, since my user is already logged in?
Thanks
Well, I believe you want the user to enter the old password just to verify whether it's the actual owner of the account or not.
Firebase handles this situation very well, you just need to call the updatePassword method on the user object and pass in the new password.
const changePassword = async newPassword => {
const user = firebase.auth().currentUser;
try {
await user.updatePassword(newPassword)
console.log('Password Updated!')
} catch (err) {
console.log(err)
}
}
If it's been quite a while that the user last logged in then firebase will return back an error -
"This operation is sensitive and requires recent authentication. Log in before retrying this request."
Thus, you don't really need to check the old password as firebase does it for you.
But if you just want to do it in one go, without having the user to log in again.
There's a way for that as well.
There is a method on user object reauthenticateAndRetrieveDataWithCredential you just need to pass in a cred object(email and password) and it refreshes the auth token.
const reauthenticate = currentPassword => {
const user = firebase.auth().currentUser;
const cred = firebase.auth.EmailAuthProvider.credential(
user.email, currentPassword);
return user.reauthenticateAndRetrieveDataWithCredential(cred);
}
In your particular case, you can have something like this
const changePassword = async (oldPassword, newPassword) => {
const user = firebase.auth().currentUser
try {
// reauthenticating
await this.reauthenticate(oldPassword)
// updating password
await user.updatePassword(newPassword)
} catch(err){
console.log(err)
}
}
Learn more about firebase reauth - https://firebase.google.com/docs/auth/web/manage-users#re-authenticate_a_user
Hope it helps
Not able to link facebook account with an existing firebase account. I currently have a firebase account in that I created using google credentials. Now I want to link a facebook account with this exiting firebase account (both having same creadentials) and I followed the steps mentioned here :
https://firebase.google.com/docs/auth/web/facebook-login
But at last, when I call method "result.user.link(pendingCred).then( (user) => { .. }" to link account I get following error in console :
Uncaught TypeError: result.user.link is not a function
at auth.service.ts:188
at e.g (auth.js:23)
at Yb (auth.js:26)
at Ub (auth.js:26)
at z.webpackJsonp.../../../../#firebase/auth/dist/auth.js.h.Mb (auth.js:25)
at Cb (auth.js:19)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:392)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:142)
at zone.js:873
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:425)
Here is my code
loginFacebook(): Promise<any> {
return this.afAuth.auth.signInWithPopup(new firebase.auth.FacebookAuthProvider())
.then( (result) => {
this.registerUserName = result.user.displayName;
this.authDbUsername = result.user.email.replace('.', '');
// this.setLoggedinUser(user)
})
.catch ( (error) => {
if (error.code === 'auth/account-exists-with-different-credential') {
alert('You have already signed up with a different auth provider for that email.');
const pendingCred = error.credential;
// The provider account's email address.
const email = error.email;
// Get registered providers for this email.
firebase.auth().fetchProvidersForEmail(email).then( (providers) => {
// Step 3.
// If the user has several providers,
// the first provider in the list will be the "recommended" provider to use.
if (providers[0] === 'password') {
// Asks the user his password.
// In real scenario, you should handle this asynchronously.
const password = this.promptUserForPassword(providers[0]); // TODO: implement promptUserForPassword.
firebase.auth().signInWithEmailAndPassword(email, password).then( (user) => {
// Step 4a.
return user.link(pendingCred);
}).then(function( user) {
// Google account successfully linked to the existing Firebase user.
this.authState = user;
});
return;
}
// All the other cases are external providers.
// Construct provider object for that provider.
// TODO: implement getProviderForProviderId.
const provider = new firebase.auth.GoogleAuthProvider(); // this.getProviderForProviderId(providers[0]);
// At this point, you should let the user know that he already has an account
// but with a different provider, and let him validate the fact he wants to
// sign in with this provider.
// Sign in to provider. Note: browsers usually block popup triggered asynchronously,
// so in real scenario you should ask the user to click on a "continue" button
// that will trigger the signInWithPopup.
firebase.auth().signInWithPopup(provider).then( (result) => {
// Remember that the user may have signed in with an account that has a different email
// address than the first one. This can happen as Firebase doesn't control the provider's
// sign in flow and the user is free to login using whichever account he owns.
// Step 4b.
// Link to Google credential.
// As we have access to the pending credential, we can directly call the link method.
const resultingUser = result.user;
result.user.link(pendingCred).then( (user) => {
// Google account successfully linked to the existing Firebase user.
this.authState = user; // goToApp();
}).catch( (errorInLinking) => {
console.log(errorInLinking);
});
});
});
}
});
}
Please let me know if I am missing anything.
Thanks!
Change to result.user.linkWithCredential(pendingCred).then( (user) ...
link has been deprecated in favor of linkWithCredential starting from version 4.0.0.