I'm using Meteor User accounts with multiples services (google, facebook, ...)
I don't understand why when I'm registering with google and facebook with the same email address, mongodb creating 2 different accounts.
I'm using the default Meteor User accounts settings.
(I have no code to show you)
Meteor doesn't merge user accounts from different services based on email address. If the user's login credentials aren't present in the database yet, a new user gets created. Automatically merging Facebook, Google, and email/password credentials is a potential security hole.
However, I believe it's possible to merge them manually. Login credentials are stored in db.users.services document, and it should be possible to have more than one login method per user. I wouldn't recommend that though.
If you want to ensure the same email isn't used twice, you can do this:
Accounts.onCreateUser(function(options, user) {
var service, serviceName;
if (!user.profile) {
user.profile = options.profile || {};
}
if(user.services) {
// Get first service
serviceName = _.keys(user.services)[0];
user.meta.service = serviceName;
if (!user.emails || !user.emails.length) {
if(serviceName === "facebook" || serviceName === "google") {
service = user.services[serviceName];
user.emails = user.emails || [];
user.emails[0] = {
address: service.email,
verified: service.verified_email
};
}
}
}
return user;
});
This will add the email field from the provider in the users "email" field, and it will prevent users from registering twice with two different accounts.
Related
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.
I m running a Wordpress site and I have set up Auth0 passwordless log in using the Lock Passwordless UI.
The passwordless authentication does not allow additional fields for the sign up and I successfully wrote a rule (not sure if is OK but it works) that redirects the user that logs in for the first time to a sign up form on Wordpress asking for more details from them.
function (user, context, callback) {
const namespace = 'https://mysiteurl/redirect';
const userType = context.stats && context.stats.loginsCount === 1 ? 'new' : 'existing';
context.idToken[`${namespace}userType`] = userType;
if (userType === 'new') {
context.redirect = {
url: "https://mys-site-url/sign-up-wordpress-form"
};
}
return callback(null, user, context);
}
What I would like to have is a permanent registration form on Wordpress where I collect data and the sign in process to be passwordless with email only.
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)
}
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.
The application I am building is designed so user's register with just an email address and password, but when they login, it is required that they then fill in a username and birthday.
I have created a route group using FlowRouter for authenticated users:
var authRoutes = FlowRouter.group({
name: 'auth',
triggersEnter: [function(context, redirect) {
// Is the user logging in or already logged in?
if(Meteor.loggingIn() || Meteor.userId()) {
//They are, so track when user is available
Tracker.autorun(function() {
if(Meteor.user()) {
// User is available
}
});
} else {
// They are not
FlowRouter.redirect('/login');
}
}],
});
However, this seems like the wrong way to go about this (having to track when the user is available in the route group). Is there a different way to achieve the same thing?