Is there a way to generate a firebase email verification link before a user is actually signed up? - firebase

I am currently implementing a MFA system with Firebase Authentication & Google Authenticator.
Since my users are not allowed to authenticate with a non-verified email address, I'd like to prevent them from signing-in if their Firebase Authentication email_verified is set to false. To do that, I am using Google Cloud Identity Provider blocking functions, this works perfectly.
However, when it comes to the registration beforeCreate blocking function hook, I can't find a way to generate an email verification link for the user currently being created, the documentation says:
Requiring email verification on registration The following example
shows how to require a user to verify their email after registering:
export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
const locale = context.locale;
if (user.email && !user.emailVerified) {
// Send custom email verification on sign-up.
return admin.auth()
.generateEmailVerificationLink(user.email)
.then((link) => {
return sendCustomVerificationEmail(
user.email, link, locale
);
});
}
});
export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
}
});
However, as far as I understand, generateEmailVerificationLink() can only be called to generate email verification link of an existing Firebase Authentication user. At this stage (while running beforeCreate blocking function), the user is not created yet.
Now I am wondering, I am missing something or is the Google documentation wrong?

No.
User data is created upon registration in the database.
Then, you may send an Email-Verification with a link automatically.
This Email-Verification just updates the field emaiVerified of said user data.
If you want to prevent users with unverified Emails from logging in, you need to adjust your Login page and check whether emaiVerified is true.
Important: Google will sign in a user right upon registration whether the email is verified or not, as this is the expected behavior from the perspective of a user. Email verification is ensured on the second, manual login.
(Also, please do not screenshot code.)

You can let a user sign in via email link at first, and call firebase.User.updatePassword() to set its password.
I am using Angular-Firebase, this is the logic code.
if (this.fireAuth.isSignInWithEmailLink(this.router.url)) {
const email = this.storage.get(SIGN_IN_EMAIL_KEY) as string;
this.storage.delete(SIGN_IN_EMAIL_KEY);
this.emailVerified = true;
this.accountCtrl.setValue(email);
from(this.fireAuth.signInWithEmailLink(email, this.router.url)).pipe(
catchError((error: FirebaseError) => {
const notification = this.notification;
notification.openError(notification.stripMessage(error.message));
this.emailVerified = false;
return of(null);
}),
filter((result) => !!result)
).subscribe((credential) => {
this.user = credential.user;
});
}
const notification = this.notification;
const info = form.value;
this.requesting = true;
form.control.disable();
(this.emailVerified ? from(this.user.updatePassword(info.password)) : from(this.fireAuth.signInWithEmailLink(info.account))).pipe(
catchError((error: FirebaseError) => {
switch (error.code) {
case AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY.POPUP_CLOSED_BY_USER:
break;
default:
console.log(error.code);
notification.openError(notification.stripMessage(error.message));
}
this.requesting = false;
form.control.enable();
return of(null);
}),
filter((result) => !!result)
).subscribe((result: firebase.auth.UserCredential) => {
if (this.emailVerified) {
if (result.user) {
notification.openError(`注册成功。`);
this.router.navigateByUrl(this.authService.redirectUrl || '');
} else {
notification.openError(`注册失败。`);
this.requesting = false;
form.control.enable();
}
} else {
this.storage.set(SIGN_IN_EMAIL_KEY, info.account);
}
});

Mate, if database won't create a new user using his email and password, and you send him email verification which will create his account, how the heck database will know his password? If it didn't create his account in the first step? Stop overthinking and just secure database using rules and routes in application if you don't want user to read some data while he didn't confirm email address.
It is that simple:
match /secretCollection/{docId} {
allow read, write: if isEmailVerified()
}
function isEmailVerified() {
return request.auth.token.email_verified
}

I think the blocking function documentation is wrong.
beforeCreate: "Triggers before a new user is saved to the Firebase Authentication database, and before a token is returned to your client app."
generateEmailVerificationLink: "To generate an email verification link, provide the existing user’s unverified email... The operation will resolve with the email action link. The email used must belong to an existing user."
Has anyone come up with a work around while still using blocking functions?
Using firebase rules to check for verification isn't helpful if the goal is to perform some action in the blocking function, such as setting custom claims.

Related

Linking social provider with anonymous user on two devices

When a user start using our app we log him in using Firebase anonymous login.
We later allow them to login with social providers like Apple. We use the "auth().currentUser?.linkWithCredential" to link the social credentials with the anonymous user id.
We encountered a situation we are not sure how to solve:
User install the app on the device and use Sign in with Apple to sign in. We link the anonymous account to the Apple login and everything works just fine.
But now the user buys a new device. He installs the app and start it. He gets a new anonymous uid. He then try to sign in with Apple. Now if we try to call linkWithCredential we get an error:
"auth/credential-already-in-use] This credential is already associated with a different user account"
This is of course true, as the Apple credentials were associated with the anonymous user on the old device.
So how do we allow a user to sign in again from a new device?
We thought to catch the error, and then call signInWithCredential instead of linkWithCredential. But then we get an error:
Duplicate credential received. Please try again with a new credential.
It seems you can't use the Apple credentials for more than one call.
So again - we are stuck with no way to allow a user to login in two devices.
let appleAuthRequestResponse = null;
try {
appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.LOGIN,
requestedScopes: [
AppleAuthRequestScope.EMAIL,
AppleAuthRequestScope.FULL_NAME,
],
});
} catch (err) {
// TODO: decide what to do
return;
}
const {
identityToken,
nonce
} = appleAuthRequestResponse;
const appleCredential = auth.AppleAuthProvider.credential(
identityToken,
nonce
);
let userCredentials = null;
try {
userCredentials = await auth().currentUser ? .linkWithCredential(
appleCredential
);
// This will work on the first device but fail on the second one
console.log(userCredentials);
} catch (err) {
// This will fail as well with error: Duplicate credential received
await auth().signInWithCredential(appleCredential)
}

How to force logout firebase auth user from app remotely

I have a project which uses firebase auth with firebaseUI to authenticate users. I have enabled Google, Facebook and email providers. What I need is to remotely logout or disable some of the users.
I want the users to logout from the app on doing so. I tried disabling the user in the firebase console and also used the firebase admin SDK (https://firebase.google.com/docs/auth/admin/manage-sessions) to revoke the refresh tokens.
I waited for more than 2 days and still noticed that the user was logged in and could access the firestore data.
I have also gone through and tried
Firebase still retrieving authData after deletion
Can anyone point to what I am doing wrong ?
You also cannot remotely force a user to be signed out. Any sign out will have to happen from the device that the user is signed in on.
There is no way to revoke an access token once that is minted. This means that even if you disable the user's account, they may continue to have access for up to an hour.
If that is too long, the trick (as also mentioned in my answer to the question you linked) is to maintain a list of blocked users in your database (or elsewhere) and then check against that in your security rules (or other authorization layer).
For example in the realtime database, you could create a list of blocked user's UIDs:
banned_uids: {
"uid1": true
"uid2": true
}
And then check against that in your security rules with:
".read": "auth.uid !== null && !root.child('banned_uids').child(auth.uid).exists()"
You can send a message data with FCM to force to log out.
For example, if the users use android application.
Save the FCM token in a collection in firebase Realtime.
configure the Android client app, in the service. LINK You have to make when receive a message with especial string, force to log out.
make the trigger you need in cloud functions, to send the data LINK when you need the user log out.
SUCCESS!
As per your scenarios, i assume that you need to make user logout when user is disabled.
Use One global variable to store TokenNo (might be in shared preference or sqlite):
Add following code to your manifest:
<service android:name=".YourFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Add following code in your
public class LogoutOntokenchange extends FirebaseMessagingService{
#Override
public void onNewToken (String token){
if(TokenNo=>1){ //if tokenNo >=1 means he already logged in
TokenNo=0;
FirebaseAuth.getInstance().signOut(); //Then call signout method
}
else{
TokenNo=1; //store token no in db
}
}
}
What Happens here:
When user logged in first time onNewToken is called then It goes into else then TokenNo is updated to 1 from 0.
When You disable any user then automatically token is refreshed.Then OnNewToken is called then TokenNo>=1 so user will be logged out.
NOTE: When user log in for first time i.e if TokenNo variable is not stored then store it as 0.
For reference: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService
The only way I can think about is adding a if-else block in your starting activity.
Store the that status of user (verified/banned/deleted) in Firebase Real-time database. Then retrieve the status of user at start of application and add the code:
if (currentUserStatus.equals("banned"))
{
currentUser.logout();
}
What I've done is I created for each user upon registration a Firestore document with the UID as document ID. In this document I store an array which stores all fcm tokens the individual user receives when logging into a new device. That way I always keep track where the user is logged in. When the user logs out manually the fcm token will be deleted from the document in Firestore as well as on the device.
In order to be able to log out the user everywhere they are signed in I did the following. When starting the app and once the user is logged in I start a snapshot listener that listens to all changes in the users document. As soon as there is a change I retrieve the new array of fcm tokens, search inside the array for the local current device fcm token. If found, I do nothing. If the fcm token is no longer in the array I will call the local logout method and go back to the login screen.
Here are the methods I used in swift on iOS. The closures (passOnMethod) will just trigger an unwind segue to the login view controller.
import Foundation
import Firebase
class FB_Auth_Methods {
let db = Firestore.firestore()
var listener: ListenerRegistration?
func trackLoginStatus(passOnMethod: #escaping () -> () ) {
listener?.remove()
if let loggedInUserA_UID = Auth.auth().currentUser?.uid {
listener = db.collection(K.FStore.collectionOf_RegisteredUsers_Name)
.document(loggedInUserA_UID)
.addSnapshotListener { (snapshotDocument, error) in
if let error = error {
print(error)
} else {
if let document = snapshotDocument {
if let data = document.data() {
if let fcmTokens = data[K.FStore.Users.fcmTokens] as? [String] {
print("Found the following tokens: \(fcmTokens)")
self.compareTokensAgainstCurrentDeviceToken(fcmTokens: fcmTokens, passOnMethod: { () in
passOnMethod()
})
}
}
}
}
}
}
}
func compareTokensAgainstCurrentDeviceToken(fcmTokens: [String], passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
if fcmTokens.contains(result.token) {
print("Token found, doing nothing")
} else {
print("Token no longer found, logout user")
do {
try Auth.auth().signOut()
InstanceID.instanceID().deleteID { error in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
} catch let signOutError as NSError {
print (signOutError)
}
}
}
}
}
}
And here is the method I use when logging out the user everywhere but at the current device.
func deleteAllFcmTokensExceptCurrent(loggedInUserA: User, passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
let batch = self.db.batch()
let deleteAllFcmRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.delete()], forDocument: deleteAllFcmRef)
let updateFcmTokenRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.arrayUnion([result.token])], forDocument: updateFcmTokenRef)
batch.commit { (error) in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
}
}
}
Not tested yet, as our backend programmer, who is in charge of setting up Firestore rules was gone for the day, but in theory this should work: (and it's something I'll test tomorrow)
Having a FirebaseAuth.AuthStateListener in charge of serving UI based on the status of the user
This combined with rules in firestore
match /collection
allow read: if isAuth();
Where isAuth is:
function isAuth() {
return request.auth.uid != null;
}
If the user is then disabled, while being logged in, whenever the user tries to read data from the collection, he should be denied, and a signOut() call should be made.
The AuthStateListener will then detect it, and sign the user out.

Check if an email already exists in Firebase Auth in Flutter App

I'm currently developing a flutter app that requires users to register before using it. I use Firebase Authentication and would like to check whether an email is already registered in the app.
I know the easy way to do it is to catch the exception when using the createUserWithEmailAndPassword() method (as answered in this question). The problem is that I ask for the email address in a different route from where the user is registered, so waiting until this method is called is not a good option for me.
I think the best option would be to use the method fetchProvidersForEmail(), but I can't seem to make it work.
How do I use that method? Or is there a better option to know if an email is already registered?
The error raised is a PlatformException
so you can do something as follows-
try {
_firbaseAuth.createUserWithEmailAndPassword(
email: 'foo#bar.com',
password: 'password'
);
} catch(signUpError) {
if(signUpError is PlatformException) {
if(signUpError.code == 'ERROR_EMAIL_ALREADY_IN_USE') {
/// `foo#bar.com` has alread been registered.
}
}
}
The following error codes are reported by Firebase Auth -
ERROR_WEAK_PASSWORD - If the password is not strong enough.
ERROR_INVALID_EMAIL - If the email address is malformed.
ERROR_EMAIL_ALREADY_IN_USE - If the email is already in use by a different account.
There is no such fetchProvidersForEmail method anymore in the current version of the firebase auth package. The equivalent one is now fetchSignInMethodsForEmail method which I think would be the best option to handle this case without executing any unnecessary operation.
fetchSignInMethodsForEmail
In docs, it's stated that this method returns an empty list when no user found, meaning that no account holds the specified email address:
Returns a list of sign-in methods that can be used to sign in a given
user (identified by its main email address).
This method is useful when you support multiple authentication
mechanisms if you want to implement an email-first authentication
flow.
An empty List is returned if the user could not be found.
Based on this, we could create our own method like the following one:
// Returns true if email address is in use.
Future<bool> checkIfEmailInUse(String emailAddress) async {
try {
// Fetch sign-in methods for the email address
final list = await FirebaseAuth.instance.fetchSignInMethodsForEmail(emailAddress);
// In case list is not empty
if (list.isNotEmpty) {
// Return true because there is an existing
// user using the email address
return true;
} else {
// Return false because email adress is not in use
return false;
}
} catch (error) {
// Handle error
// ...
return true;
}
}
I think the only possibility from within the app is attempting a login (signInWithEmailAndPassword) with that e-mail and check the result.
If it's invalid password, the account exists.
If it's invalid account, the account do not exist.
Error 17011
There is no user record corresponding to this identifier. The user may have been deleted
Error 17009
The password is invalid or the user does not have a password
As this is a kind of an ugly solution, you can justify this additional call using it to check it the e-mail formatting is correct (according to the firebase rules). If it doesn't comply it will throw a address is badly formatted and you can alert the user soon enough.
You can do these checks using the error codes with current versions of the plug-in.
There are many ways you can do that. As Sakchham mentioned, you could use that method. There is another method you could use which in my opinion is better and safer.
Since the password value will return ERROR_WEAK_PASSWORD, it is a create account method which you are calling which means that it's possible an account will be created if the account doesn't exist, in that case, I recommend personally using the sign in with email method.
I used this code below:
Future<dynamic> signIn(String email) async {
try {
auth = FirebaseAuth.instance;
await auth.signInWithEmailAndPassword(email: email, password: 'password');
await auth.currentUser.reload();
return true;
} on FirebaseAuthException catch (e) {
switch (e.code) {
case "invalid-email":
return 'Your username or password is incorrect. Please try again.';
break;
}
}
}
Leave down a comment if you have any suggestions.
I didn't think fetchProvidersForEmail() method is available in the firebase package. So we can show the appropriate message to the user. you can create more case if you need.
try {
await _auth.signInWithEmailAndPassword(
email: "Hello#worl.com",
password: "123456789"
);
} catch (e) {
print(e.code.toString());
switch (e.code) {
case "email-already-in-use":
showSnackBar(context,"This Email ID already Associated with Another Account.");
break;
}
}

Firebase, login by same email different provider

I want my users can login using many different provider but they will get same result if they use only one email address. For example, in stackoverflow I can login by Facebook, Google ... but I still can keep my profile as well as my posts ...
In Firebase Web, for example if my user created an account with email/password provider, his email="ex#gmail.com" password="123456". This account has uid="account1" and I use this uid as a key to store additional information about him in Firebase Database.
Once day, he choose login by Google provider and Facebook provider (still ex#gmail.com), I test with 2 cases in auth setting:
"Prevent creation of multiple accounts with the same email address": new Google login will override old "account1" and I can not create new Facebook account with "ex#gmail.com" due to error: "An account already exists with the same email address". Which both I don't want to happend
"Allow creation of multiple accounts with the same email address": with this option I can create many account with same email address but they have diffrent uid and I don't know how to link these uid to "account1"? I also can't get email (email = null) after login by Google and Facebook.
So can firebase help me do the thing that I love in many App (login by many different ways but same result)?
This is supported by Firebase Auth through the "Prevent creation of multiple accounts with the same email address" setting. However, Google is a special case as it is a verified provider and will overwrite unverified accounts with the same email. For example, if an email/password account is created and not verified and then a user signs in with Google with the same email, the old password is overridden and unlinked but the same user (same uid) is returned.
For other cases, this is how it is handled.
Let's say you sign in with Email/Password using an email/password with account user#example.com. A user then tries to sign in with a Facebook provider using the same email. The Auth backend will throw an error and linking will be required. After that is done, a user can sign in with either accounts.
Here is an example:
var existingEmail = null;
var pendingCred = null;
var facebookProvider = new firebase.auth.FacebookAuthProvider();
firebase.auth().signInWithPopup(facebookProvider)
.then(function(result) {
// Successful sign-in.
});
.catch(function(error) {
// Account exists with different credential. To recover both accounts
// have to be linked but the user must prove ownership of the original
// account.
if (error.code == 'auth/account-exists-with-different-credential') {
existingEmail = error.email;
pendingCred = error.credential;
// Lookup existing account’s provider ID.
return firebase.auth().fetchProvidersForEmail(error.email)
.then(function(providers) {
if (providers.indexOf(firebase.auth.EmailAuthProvider.PROVIDER_ID) != -1) {
// Password account already exists with the same email.
// Ask user to provide password associated with that account.
var password = window.prompt('Please provide the password for ' + existingEmail);
return firebase.auth().signInWithEmailAndPassword(existingEmail, password);
} else if (providers.indexOf(firebase.auth.GoogleAuthProvider.PROVIDER_ID) != -1) {
var googProvider = new firebase.auth.GoogleAuthProvider();
// Sign in user to Google with same account.
provider.setCustomParameters({'login_hint': existingEmail});
return firebase.auth().signInWithPopup(googProvider).then(function(result) {
return result.user;
});
} else {
...
}
})
.then(function(user) {
// Existing email/password or Google user signed in.
// Link Facebook OAuth credential to existing account.
return user.linkWithCredential(pendingCred);
});
}
throw error;
});
The above code by #bojeil is correct Except it misses out one line.
var existingEmail = null;
var pendingCred = null;
**var facebookprovider = new firebase.auth.FacebookAuthProvider();**
firebase.auth().signInWithPopup(facebookProvider)
you have to initialize facebookprovider before passing it as an argument.

Firebase authentication (email/password) how to set user's uid?

I am authenticating using email/password like so:
firebase.auth().createUserWithEmailAndPassword(email, password).catch(function(error) {
// Handle Errors here.
});
And listening to auth here:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
console.log(user);
} else {
// No user is signed in.
console.log("Not signed in");
}
});
Which works fine. On examining the user object that auth returns, the uid is a random string, is there a way to set this uid when I create the account? For example, uid="someUserName"?
Thanks
Firebase Authentication is not like a database where you can add properties and such. It handles UID's and such for you. What you can do, is in your Firebase Database add a users directory and store additional info there (such as a username) with the UID as the key.
Here's an example:
If you're going to use this often, it's probably a good idea to go into your database rules and add an index on this username:

Resources