How can I solve create account problem with different providers? - firebase

I have a sign in with Google:example#gmail.com
then create an account with the same email:example#gmail.com
There is a problem with two different providers
Sign in with Google (same Gmail)
Sign in with Email (same Gmail)
How Can I handle these two (When I delete the google sign-in account from Firebase Console. I can create an account with that email) Otherwise I can't create an account with that email and also can't sign in.
I learning Firebase Auth with https://github.com/gladly-team/next-firebase-auth

If you first sign in with Google using "example#gmail.com", it means a user will be created using this particular email address. If you try to sign in with any other provider or with an email and password using the same email address, you'll get an error message that says that the user already exists. And it makes sense since you have already used that email for a user before.
There are two ways in which you can solve this problem. When you get such an error, you can check the provider used to create the account, and notify the user to use it. For example, if the user signs in with Google and tries to authenticate with email and password right after that, display a message to the user in which you should say that the user already exists, and should use the authentication provider which was selected to create the account in the first place, in this case, Google.
The second option would be to allow the user to have multiple accounts using the same email address with different authentication providers. This option can be enabled directly in the Firebase Console, in the Authentication section.
So it's up to you to decide which option works better for your project.

The simple Solution is to enable multiple account an email.
Or ----------------
You Link the account.
This is an example when there is a facebook account with a certain email
and you want to use that same email to sign in with Email and password or gmail, if those two emails are not linked different provider error will be thrown. check here for more
export function linkFaceBookAccount(authContext?: AuthContextType, notificationContext?: NotificationContextType, history?: History.History) {
const provider = new FacebookAuthProvider(); // create a provider
linkWithPopup(auth.currentUser as User, provider).then((result) => {
// This gives you a Google Access Token. You can use it to access the Google API.
// const credential = FacebookAuthProvider.credentialFromResult(result);
// const token = credential?.accessToken;
// The signed-in user info.
const user = result.user;
saveUserToLocalStorage(user);
authContext?.loadUserToState(user);
notificationContext?.addNotification({
message: `This email's (${auth.currentUser?.email}) account has been successful linked with your facebook account`,
title: "Link successful",
notificationType: "SUCCESS",
positiveActionText: "continue",
positiveAction: () => {
history?.push("/")
}
})
}).catch((error) => {
const email = error.customData?.email;
const errorCode = error.code;
const duplicateAccount = errorCode === "auth/account-exists-with-different-credential";
notificationContext?.addNotification({
message: errorFirebase(error, email),
title: "Linking Error",
notificationType: "WARNING",
positiveActionText: duplicateAccount ? "Link" : "ok",
negativeActionText: duplicateAccount ? "cancel" : undefined,
code: errorCode,
positiveAction: () => {
if (duplicateAccount) {
duplicateAccountLinking(email, "FACEBOOK", history);
}
}
})
});}

Related

Resending Firebase auth verification emails when the user's email is no longer accessible

So far in my project, I have set up a basic user management system. However, I'm unable to figure out a way to resend verification link after the user registers.
For example: When the user signs up, createUserWithEmailAndPassword is called which returns a promise. The promise is now resolved using then (or await) to which sendEmailVerification is called. This is all fine.
Note: The above flow is what I currently have implemented to for user management on the client side with Firebase Auth.
However, what if the user happens to delete this email or for whatever reason has no access to it at all. I want to be able to resend the link.
This uses Firebase Admin SDK on the backend and is an example of how to generate the verification email on the server-side. However, it appears that it is used in conjunction with account creation. In addition, it appears that Firebase Auth follows the same set of restrictions.
Not too sure where to go next and was wondering if there are any suitable workarounds.
Thanks.
Add a link to the login page to resend the verification email.
Then, trigger something along these lines:
sendEmailVerification() async {
await FirebaseAuth.instance.currentUser?.sendEmailVerification();
}
Another option is to check during the login process whether the user verified the email. If not, resend it. Along these lines:
signInWithEmailAndPassword(
String email,
String password,
) async {
try {
final credential = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
if (credential.user!.emailVerified == false) {
await _sendEmailVerification();
return ... // not verified, but email sent
}
return ... // success
} on FirebaseAuthException catch (e) {
return ... // error
} catch (e) {
return ... // error
}
}
The problem described here I think is as follows ( I am facing it as well):
=> some new User enters his Email and Password to create an account
=> we call createUserWithEmailAndPassword(email, password) => account can be found in firebase console under "Authentication" => in the app Auth.currentUser is NOT NULL. This is because acc. to documentatoin of "createUserWithEmailAndPassword" we read:
"#remarks
On successful creation of the user account, this user will also be signed in to your application."
=> Then we call sendEmailVerification(Auth.currentUser) - everything works, User Auth.currentUser gets his Email with verification link.
BUT. What if he does not click this link (maybe it went to spam)? He searcehs it, time passes, he maybe switches off the PC or closes the App. And after some time tries again: opens the App, tries to register again...
=> as he enters again the same E-mail he entered when he tried to register for the first time, our createUserWithEmailAndPassword() will give an error (because the User with such E-mail, even though it is not verified, is already in the database) => Auth.currentUser will be NULL(!!!)
=> if now you try to "REsend the verification E-Mail" with sendEmailVerification(Auth.currentUser) - it will not send anything. Because the user is null.
So, the way out of this situation is to generate verficication link based on the E-mail only and send it somehow to the User via E-mail: https://firebase.google.com/docs/auth/admin/email-action-links#java_2

Generate undo email change link in firebase cloud functions

How can I generate a link to undo the email change in firebase cloud functions?
So when a user changes their email address, I want to generate a link to include in an automated email for them to click to undo this email change. Firebase sends an email when an email changes, but I want to be able to do that myself with my own code.
Currently, I can find that there are ways you can generate a link to change the user password, verify email, etc. However, I cannot find a method that I can use to generate a link to undo email change when the user changes their email.
When a user changes the email, you can store a document in Firestore containing their old email, a token and some metadata if you need to. That being said, you should update user's email from a Cloud function or your server using the Admin SDK only as there are no triggers on E-Mail change.
import jwt from "jsonwebtoken"
import {v4} from "uuid"
exports.changeEmail = functions.https.onCall(async (data, context) => {
const {newEmail} = data;
const {uid} = context.auth;
// change user's email
// send an email to verify new email is required
// generate a JWT
const token = jwt.sign({ uid, eventId: v4() }, 'jwt_signing_secret', { expiresIn: '24h' });
// add a document in Firestore containing details about this event
await admin.firestore().collection("emailChanges").doc(eventId).set({
uid, changedAt: Date.now()
})
const undoURL = `https://[YOUR_DOMAIN]/revert-email-change?token=${token}`
// E-Mail this URL to user
// Terminate this function
})
Replace [YOUR_DOMAIN] will the URL of your website. Once the user visits /revert-change-email email page of your website, call another function that verifies this token.
exports.revertEmailChange = functions.https.onCall((data, context) => {
// pass the token from frontend by checking URL params
const {token} = data
// Verify the token
const decoded = jwt.verify(token, 'jwt_signing_secret');
console.log(decoded)
const {uid, eventId} = decoded
// token is valid
// read the Firestore document using stateId and check old email
const snap = await admin.firestore().collection("emailChanges").doc(eventId).get()
if (!snap.exists) return {error: "Invalid Token"}
const {email} = snap.data()
// use updateUser() method to change email back
// delete that document from Firestore
return {data: "Email changed back successfully"}
});
You can change the lifespan of JWT token i.e. how long the URL should be valid. You can read more about JWT at jwt.io. The additional eventId token is just to prevent that JWT token so it cannot be reused.
When writing Cloud Functions for Firebase, one uses the Admin Node.js SDK.
AFAIK it is not possible, with this Admin SDK, to generate an email action link to undo an email change, as we can we can do, for example, for email verification with the generateEmailVerificationLink() method.
You will need to build your own mechanism yourself. You'll probably have to save somewhere (e.g. in Firestore) the previous email and expose an HTTP endpoint to trigger the action (HTTPS Cloud Function? Call to the Firestore REST API?). In any case you'll have to check the identity of the calling user (by either checking the Firebase ID token as a Bearer token in the Authorization header of the HTTP request or via a dedicated Firestore Security Rule).
There isn't enough details in your question to understand the exact flow of your complete use case (i.e. from the request to change email up to the action of undoing an effective change) and propose a sensible approach.

Can I skip the first step in the passwordless signin method in Firebase?

I have a list of people with all their personal information (name, first name, date of birth, email, etc.).I want to send to each of these people an email with a link allowing them, once clicked, to be directly connected on our website. Without having to type a password.
I followed the Firebase procedure for passwordless authentication:
from the back back in Python. Generate email link for connexion
for the front in Angular Js. Completing signin in a web page
Fireship.io tutorial
But most of the examples don't quite fit my use case.
Most of the examples:
User comes to your website, asks for passwordless authentication, types in his email, (the email is stored in window.location.href)
User receives an email with a link to log in, he clicks on it
User is on your website, logged in (thanks to his email stored in window.location.href).
My use case:
None. I already have the email of my user, so I send him directly the link to connect.
User receives an email with a link to log in, he clicks on it
User is on my website, but has to type his e-mail again in the prompt (because it is obviously not stored in window.location.href).
In my case the window.location.href variable will never be used. And I don't want my user to have to retype his email once the link is clicked. Since I already have his email, why ask him again?
So how can I skip this step? Is there any security risk in doing so?
This is my code so far:
Back:
import firebase_admin
from firebase_admin import auth
from google.cloud import firestore
def create_new_auth(dictionary):
user = auth.create_user(
email=dictionary['email'],
email_verified=True,
phone_number=dictionary['phone'],
password='super_secure_password_007',
display_name=f"{dictionary['firstName']} {dictionary['lastName']}",
disabled=False)
print('Sucessfully created new user: {0}'.format(user.uid))
return user.uid
def create_new_pre_user(db, dictionary, uid):
dictionary = {
'uid': uid,
'email': dictionary['email'],
'lastName': dictionary['lastName'],
'gender': dictionary['gender'],
'birthday': dictionary['birthday'],
'phone': dictionary['phone'],
'firstName': dictionary['firstName']
}
db.collection(u'users').document(uid).set(dictionary)
def main(dictionary):
firebase_admin.initialize_app()
db = firestore.Client()
uid = create_new_auth(dictionary)
create_new_pre_user(db, dictionary, uid)
action_code_settings = auth.ActionCodeSettings(
url=f'http://localhost:4200/login',
handle_code_in_app=True,
ios_bundle_id='com.example.ios',
android_package_name='com.example.android',
android_install_app=True,
android_minimum_version='12',
dynamic_link_domain='magic42.page.link',
)
link = auth.generate_sign_in_with_email_link(dictionary['email'], action_code_settings)
if __name__ == '__main__':
dictionary = {
"firstName": "Jone",
"lastName": "Doe",
"birthday": 12345678,
"gender": "male",
"email": "john.doe#gmail.com",
"phone": "+33611223344"
}
main(dictionary)
Front:
private signInWithEmail() {
if (this.authService.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.
let 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.
this.authService.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
this.router.navigate(['/patient', 'quiz'])
})
.catch((error) => {
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
}
}
isSignInWithEmailLink(href) {
return this.afAuth.auth.isSignInWithEmailLink(href);
}
signInWithEmailLink(email: string, href: string) {
return this.afAuth.auth.signInWithEmailLink(email, href)
}
EDITS
The problem is that the front has no knowledge of the user email when he first come the our website using the link. There is a way to pass the email information from our server-side to the front but it's in clear in the URL : that's risky and not a good practice according to Firebase itself (link)
Like this:
def main(dictionary):
firebase_admin.initialize_app()
db = firestore.Client()
uid = create_new_auth(dictionary)
create_new_pre_user(db, dictionary, uid)
action_code_settings = auth.ActionCodeSettings(
url=f'http://localhost:4200/login/?email=john.doe#gmail.com',
handle_code_in_app=True,
ios_bundle_id='com.example.ios',
android_package_name='com.example.android',
android_install_app=True,
android_minimum_version='12',
dynamic_link_domain='magic42.page.link',
)
link = auth.generate_sign_in_with_email_link(dictionary['email'], action_code_settings)
So how can I pass the email information from the back to the front so that the user doesn't have to type it again when redirected to my website after clicking to my "magic link" ?
One thing you could do is to create a single-use token on the backend that links to your user's email (or that links to a document in firestore) and have that be in the url. When the user enters the page, make a call to your backend with the token (could be just a simple uuid) and have your backend sign the user in and then expire/remove that token from use.
E.G.
https://yoursite.com/44ed3716-2b8f-4068-a445-b05a8fee17c3
Frontend sends 44ed3716-2b8f-4068-a445-b05a8fee17c3 to backend...backend sees the token, logs them in, then makes that token no longer valid.
Update
To answer your question in the comments below about not needing email link auth anymore through firebase: not necessarily. At that point, you're kind of creating your own email sign-in system (which actually isn't too hard) and somewhat re-inventing the wheel. Adding a token to the url was just a way for you to associate the user with an email without having to actually put the email in the url so that your frontend can know who the user is once your link is clicked. Once the backend sends you the email, you can store it local storage and complete the sign in with firebase normally.
There's no security risk asking the user to type his/her email versus storing it in the window storage, and one could argue that it's actually more secure to do so. That said, how you can go about doing this:
Make sure you have enabled email passwordless authentication.
Using the admin SDK, add each email address to your auth table (though I wouldn't set emailVerified: true - that will happen when they click the magic link and verify themselves on login.
Again using the admin SDK, generate a magic link for each user and send it to them.
On your login page (where the magic link takes them), prompt them for their email address and then use that along with the magic link to authenticate. The sample code provided from Firebase shows you how to do this in the if(!email){ ... } part of the code where it uses a window prompt to collect the user's email in case the user clicked the link on a separate device or the browser didn't/couldn't store the email address.
If you already have the user's email, you can call firebase.auth().sendSignInLinkToEmail with that email in the client-side JavaScript SDK (docs), or generateSignInWithEmailLink in the server-side Node.js SDK (docs). Both calls take the user's email as an argument.
Once the user lands on your site after clicking the link, you can access their profile with an auth state listener like this:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
var uid = user.uid;
var email = user.email;
}
});

Firebase user.delete() method works but with error

I am using angular, and have an application that stores user details, and login info. When trying to delete a user, I am first deleting all the user related information. Then asking the user to re-authenticate themselves, after authentication, user gets logged out, and their basic details fetched to show profile id deleted followed by their sign-in info using user.delete().
All this works as expected, but at the end I am getting an error. Why am I am getting this error even when I have already logged out the user of the application.
Error Message: {code: "auth/user-token-expired", message: "The user's credential is no longer valid. The user must sign in again.", a: null}
My code -
deleteAccount(){
var userToDelete = firebase.auth().currentUser;
this.logout();
this.store.dispatch(UI.StartAppLoad({status:'Deleting User Details...'}));
this.userService.DeleteUser(userToDelete.uid)
.then((res)=>{
console.log(res);
}).catch(this.HandleError.bind(this));
userToDelete.delete().then(
(res)=>{
console.log(res);
this.uiService.showSnackbar('User Account Deleted',null,3000);
this.store.dispatch(UI.LoadApp());
}
).catch(this.HandleError.bind(this));
}
logout() {
this.afAuth.signOut();
}
where, HandleError is used to display the Error Message in a snackbar.
deleteAccount() is called after the user successfully authenticates themselves.
Instead of getting the error message displayed, I want to display the message 'User Account Deleted'.
Entire Flow -
onDeleteAccount(){
const confirmResult = this.uiService.showConfirm({
isDanger:true,
title:'Delete Account?',
content:'All your user account data will be permamnently deleted.'+
' You will need to create a new account later. Are you sure you want to continue?',
okText:'Delete'
});
confirmResult.subscribe(async isDelete=>{
if(isDelete){
this.store.dispatch(UI.StartAppLoad({status:'Deleting Excercise Data...'}));
const isResetDone = await this.trainingService.resetPastExercise();
if(isResetDone){
this.store.dispatch(UI.StartAppLoad({status:'Deleting Follow list...'}));
this.userService.clearFollowList();
this.authService.actionToPerform.next(actions.Delete_Account);
this.store.dispatch(UI.LoadApp());
this.router.navigate([AppRoutes.ReAuthenticate]);
}
}
});
}
Authenticate Page's submit() method:
this.authService.reauthenticate({
email:form.value.email,
password:form.value.password
});
this.authService.deleteAccount();
AuthService:
reauthenticate(authdata: AuthData) {
this.store.dispatch(UI.StartLoading());
var credential = firebase.auth.EmailAuthProvider.credential(
authdata.email,
authdata.password
);
this.afAuth.currentUser.then((user) => {
user.reauthenticateWithCredential(credential)
.then((res)=>{
this.prevPwd = authdata.password;
console.log(res);
this.store.dispatch(UI.StopLoading());
})
.catch(this.HandleError.bind(this))
});
}
And then the above method deleteAccount()
Please suggest.
As explained in the doc, this happens because:
delete() is a security-sensitive operation that requires the user to
have recently signed in.
The doc also indicates that:
If this requirement isn't met, ask the user
to authenticate again and then call firebase.User.reauthenticateWithCredential.
So, you need to handle this specific error and do as indicated by the doc, i.e. "ask the user to authenticate again and then call reauthenticateWithCredential."

Cannot pass a read permission (email) to a request for publish authorization

I am trying to use Facebook login using Firebase authentication. I have followed whole documentation. Lastly whenever I click the login button it gives an error saying:
Cannot pass a read permission (email) to a request for publish authorization
The line on which it is showing error is:
LoginManager.getInstance().logInWithPublishPermissions(LoginActivity.this,Arrays.asList("email", "public_profile"));
Can someone explain me what I am doing wrong? I have looked other answers also but no help from there.
Use logInWithReadPermissions instead of logInWithPublishPermissions. The error message is very clear about that, you are trying to request read permissions with a function that is being used for publish permissions.
.getInstance().logInWithPublishPermissions is not Firebase Authentication. Signin via federated providers such as Facebook is available via the Firebase JavaScript (web) SDK. See Authenticate Using Facebook Login with JavaScript for details.
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Facebook Access Token. You can use it to access the Facebook API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});

Resources