Firebase login with Facebook provider returns empty token after password reset - firebase

I have a project consisting of an Android and an iOS client connected to a PHP backend. Login is set up via Firebase with email, Facebook and Google providers and works OK. Password resetting also works; I obtain the link on the backend given the email and send the password reset link to the client if the email exists. The link is obtained like this:
$auth = app('firebase.auth');
$link = $auth->getPasswordResetLink($request->email); // The user provided email is passed in if it exists in the database
// Then the link is sent back to the user in an email.
Now the problem is not critical since it's not an everyday scenario, but it comes when logging in with Facebook (it doesn't happen with Google), log out, then the user clicks on "Did you forget your password?" and obtains the link, and resets the password. At this moment, in Firebase, the login provider changes back from Facebook to email as expected, but if the user tries to login again with Facebook, the signin itself doesn't throw any error (and I obtain expected user data like the Firebase ID, name, etc.) but the access and refresh tokens are empty.
This is the code I'm using to sign in the user:
$auth = app('firebase.auth');
$signInToken = $request->token; // The token obtained at the client's side, needed to perform the login in Firebase
$signInProvider = $request->provider; // This will be 'google.com' or 'facebook.com'
try {
$signInResult = $auth->signInWithIdpAccessToken($signInProvider, $signInToken, null, null, null, null);
// The following traces are all empty in the described case
Log::debug('Sign in result idToken: ' . $signInResult->idToken());
Log::debug('Sign in result accessToken: ' . $signInResult->accessToken());
Log::debug('Sign in result tokenResponse: ' . print_r($signInResult->asTokenResponse(), true));
//But this data() object indeed shows data from the user logged in:
Log::debug('Sign in result data: ' . print_r($signInResult->data(), true));
} catch(FailedToSignIn $e) {
Log::debug('Error signing in with provider: ' . $e->getMessage()); //This doesn't get called in the described scenario
}
Any idea what could be the problem?

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

How can I solve create account problem with different providers?

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);
}
}
})
});}

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: How to validate the email address when a user updates his/her email?

I'm getting an error in Firebase. When a user updates his/her email in firebase, the user's email gets updated but the user's email doesn't get validated and so the user cannot login since the email is not valid. Is there anything else that needs to be done when a user updates their email in Firebase?
It seems to be possible to send verificationEmail to the new email using sendEmailVerification on the currentUser property of auth varible in firebase. An anomally is that sometimes the mail is sent more than once at the same time, and the link gets expired.
` this.user.updateEmail(email).then(function () {
auth.currentUser.sendEmailVerification().then(() => {
this.notificationSystem.addNotification({
`message`: 'Verify your new email address with the link sent to ' + email,
level: 'info'
})
});`

Meteor.js google account : filter email and force account choser

In my Meteor.js application, I'm using the accounts-google package in order to be connected with a google account. I have two questions about it.
First, is there a simple way to filter the account used? I would like that the users can connect only with google accounts belonging to my company. Our google account mails end with #mycompany.com. So it would be a simple mail filtering.
I already done that with some post log in hooks but I was wondering if there was a simpler way for doing it.
My second question is how to force the opening of the google account choser. For now, if I try to connect with a wrong google account, and if I only added this account (like in gmail, drive, etc), the google choser doesn't pop and automatically connect with this wrong account. So, in this case, the user is totally blocked (my application disconnect him if he tries to log in with a wrong account but the google account module doesn't propose him to connect with another account).
Thank you for your help.
In order to restrict signup/login to your domain, simply do on the server:
var checkEmailAgainstAllowed = function(email) {
var allowedDomains = ['mycompanydomain.com'];
var allowedEmails = ['otheruser#fromotherdomain.com','anotheruser#fromanotherdomain.com'];
var domain = email.replace(/.*#/,'').toLowerCase();
email = email.toLowerCase();
return _.contains(allowedEmails, email) || _.contains(allowedDomains, domain);
};
Accounts.config({
restrictCreationByEmailDomain: function(email) {
if (!email) {
throw new Meteor.Error(403,'This email address is not allowed');
}
if (!checkEmailAgainstAllowed(email)) {
throw new Meteor.Error(403,'This email domain is not allowed');
}
return true;
}
});
And to login, you'll need on the client:
Meteor.loginWithGoogle({
forceApprovalPrompt: true, //this is what you want, to rerequest approval each time that prompts the google login prompt
loginStyle : "redirect", //or not, depending on your need
requestPermissions : ['profile', 'email'],
requestOfflineToken: true
}, function (err) {
if (err)
// set a session variable to display later if there is a login error
Session.set('loginError', 'reason: ' + err.reason + ' message: ' + err.message || 'Unknown error');
});
Side note:
Alternatively, you can set up your routes so that every time a new route is called, you login, and every time a route is destroyed or on windows's unload, you call logout. This causes login/logout roundtrip everytime the route changes, but you'll make sure that the new user always has a fresh session
Edit:
When you log out of your meteor app, you don't log out of google. That's how oauth works. So, basically, if you want a meteor log out to also log the user out of their google account, so that the next time they come back, they need to provide credentials again, you should do:
Meteor.logout(function(e) {
if (e) {
console.log("Could not log the user out")
} else {
window.location.replace('https://accounts.google.com/Logout');
}
});
This uses the callback of Meteor.logout() so that when the logout is successfull, the user is redirected to google's central account logout url where the user is also logged out of all google services.

Resources