I allow users to login with facebook on my app, backed by firebase authentication.
In around 20% of the facebook logins, I don't receive the user's email. I need the email address in my app, and can't figure out why I don't receive it.
Since I get the email address 80% of the time, I assume I have the right permissions setup to retrieve it.
I also enforced "One account per email address" in firebase-auth, so it seems to be a different issue than that raised in Firebase Auth missing email address.
Relevant extracts of my code:
export const FacebookSignUp: React.FC<SocialAuthProps & { title?: string }> = ({ onError, onSetWaiting, title }) => {
async function onFacebookButtonPress() {
onSetWaiting(true);
const { email, first_name, accessToken } = await getFacebookUserData();
const couldLogin = await tryLoginWithFacebook(email, accessToken);
if (!couldLogin) {
// Create a Firebase credential with the AccessToken
const facebookCredential = FacebookAuthProvider.credential(accessToken);
const userCredential = await firebaseAuth.signInWithCredential(facebookCredential);
if (userCredential.user === null) {
throw new Error("Null user");
}
const signupUser: SignupUserData = {
userId: userCredential.user.uid,
email,
pseudo: first_name || undefined
};
await createSignupUser(signupUser).then(() => {
onSetWaiting(false);
});
}
}
return (
<SocialButton
iconName="facebookIcon"
title={title || "S'inscrire avec Facebook"}
onPress={() =>
onFacebookButtonPress().catch((err) => {
onSetWaiting(false);
if (err instanceof SocialAuthError) {
onError(err);
} else if (err instanceof Error) {
const { message, name, stack } = err;
serverError("Unexpected signup error", { message, name, stack });
}
})
}
/>
);
};
import { LoginManager, AccessToken, GraphRequest, GraphRequestManager } from "react-native-fbsdk";
export async function getFacebookUserData(): Promise<FacebookInfo> {
LoginManager.logOut();
const result = await LoginManager.logInWithPermissions(["public_profile", "email"]);
if (result.isCancelled) {
throw "User cancelled the login process";
}
// Once signed in, get the users AccesToken
const { accessToken } = (await AccessToken.getCurrentAccessToken()) || {};
if (!accessToken) {
throw "Something went wrong obtaining access token";
}
return new Promise((resolve, reject) => {
let req = new GraphRequest(
"/me",
{
httpMethod: "GET",
version: "v2.5",
parameters: {
fields: {
string: "email,first_name"
}
}
},
(err, res) => {
if (err || res === undefined) {
reject(err);
} else {
const { first_name, email } = res as { first_name: string; email: string };
resolve({ first_name, email, accessToken });
}
}
);
new GraphRequestManager().addRequest(req).start();
});
}
Facebook allows you to opt out of passing your email along to third-party apps. You can request it, but the user can deny it.
If I ever log in with Facebook I always opt out of passing my email along - most of the time, the third-party app doesn't need it for legitimate purposes.
"I need the email address in my app" - why? email marketing? account duplication prevention?
In cases where you did not get an email, assume the user has opted-out and/or doesn't have an email tied to their account. If you need one, ask the user to input a contact email address and explain what you are using it for. Expect some users to still opt out and plan around it.
You could always convert their username into a non-existent email like theirusername#noreply.users.yourapp.com depending on your use case.
Related
Notice: I have seen this question, but creating a whole landing page just to verify a user seems a bit much.
I added a login functionality to my react-native app using firebase/auth with email and password. This works well so far and I have no issues doing that.
I then continued to send a verification email to a new user and only allow him/her to use the app, once the email is verified. Again, no issues here.
The next step would be to login the user right after the email was verified. This is where I'm stuck, since the onAuthStateChanged eventhandler doesn't update after the user pressed the verification link in the email.
Is there any way to listen to the emailVerified state in real-time? I tried to use polling with setInterval() but this is not great since there is a notable delay between verification and login. I read about a continueLink you can pass to sendEmailVerification, but I couldn't figure out how to make that work in react-native.
I'm using Expo and therefore the Firebase SDK, not the Firebase react native package.
Here is the code I use for the signup:
export const signUp = async (username: string, email: string, password: string) => {
try {
const auth = getAuth();
if (email && password && username) {
// sign up
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// save username in firestore
await setUserName(userCredential, username);
// send Email Verification
await sendEmailVerification(userCredential.user);
return true;
}
} catch (error) {
onError(error);
}
};
And this is my onAuthStateChanged handler:
auth.onAuthStateChanged(authenticatedUser => {
try {
if (authenticatedUser?.emailVerified) {
setUser(authenticatedUser)
} else {
setUser(null)
}
} catch (error) {
console.log(error);
}
});
So in the end I did follow this question, but I changed it a bit to fit my needs. I'll post my steps for anyone who's doing the same.
Create a simple static website with firebase init and host it on firebase or somewhere else (check the hosting tab in your firebase console to get started)
Follow this guide to create the appropriate handlers on the website
Add the following to your verificationHandler to update the user (don't forget to import firestore) (I send the userId via the continueURL, but there are probably better ways)
// You can also use realtime database if you want
firebase.firestore().collection("users").doc(userId).set({
emailVerified: true
}, {merge: true}).then(() => {
message.textContent = "Your email has been verified.";
}).catch((error) => {
message.textContent = "The verification was invalid or is expired. Please try to send another verification email from within the app.";
});
Got to authentication -> templates in your firebase console and change the action url to your hosted website's url
Add a listener to the firestore doc to your react-native app
const onUserDataChanged = (uid, callback) => {
onSnapshot(doc(firestore, "users", uid), doc => callback(doc.data()));
}
Use the data from the callback to update the login state in the app
// As an example
auth.onAuthStateChanged(authenticatedUser => {
if (authenticatedUser && !authenticatedUser.emailVerified) {
unsubscribeFirestoreListener?.();
unsubscribeFirestoreListener = onUserDataChanged(authenticatedUser.uid, (data: any) => {
if (data?.emailVerified) {
setUser(authenticatedUser);
unsubscribeFirestoreListener?.();
}
});
}
}
use the codes below for your authentication context. for user id, you should use 'user.uid'
import React, { useState, createContext } from "react";
import * as firebase from "firebase";
import { loginRequest } from "./authentication.service";
export const AuthenticationContext = createContext();
export const AuthenticationContextProvider = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
firebase.auth().onAuthStateChanged((usr) => {
if (usr) {
setUser(usr);
setIsLoading(false);
} else {
setIsLoading(false);
}
});
const onLogin = (email, password) => {
setIsLoading(true);
firebase.auth().signInWithEmailAndPassword(email, password)
.then((u) => {
setUser(u);
setIsLoading(false);
})
.catch((e) => {
setIsLoading(false);
setError(e.toString());
});
};
const onRegister = (email, password, repeatedPassword) => {
setIsLoading(true);
if (password !== repeatedPassword) {
setError("Error: Passwords do not match");
return;
}
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((u) => {
setUser(u);
setIsLoading(false);
})
.catch((e) => {
setIsLoading(false);
setError(e.toString());
});
};
const onLogout = () => {
setUser(null);
firebase.auth().signOut();
};
return (
<AuthenticationContext.Provider
value={{
isAuthenticated: !!user,
user,
isLoading,
error,
onLogin,
onRegister,
onLogout,
}}
>
{children}
</AuthenticationContext.Provider>
);
};
I made a simple registration form in React Native. I want to check if the email is valid or not by sending a verification code to it. I read many similar questions but I couldn't combine them with my code because I'm new to React Native. Can anyone help me do that without just posting a link for another answer?
Here's my registration code:
export class Register extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
name: '',
lastname: ''
}
this.onSignUp = this.onSignUp.bind(this)
}
onSignUp(){
if(
this.state.email != '' &&
this.state.password != '' &&
this.state.name != ''
){
const { email, password, name } = this.state;
firebase.auth().createUserWithEmailAndPassword(email, password)
.then((result) => {
firebase.firestore().collection("Users")
.doc(firebase.auth().currentUser.uid)
.set({
name,
email
})
console.log(result)
})
.catch((error) => {
console.log(error)
})
}
else{
alert("Please fill the empty spaces!");
}
}
render() { form body }
You cannot verify user email using before creating the account by using Client SDK only. If you need to do that, you will have to use Cloud Functions (or your own server) along with a third party email sending service and implement your own logic.
However you can verify the email of user by sending the user a verification email like this:
//new user created
var user = firebase.auth().currentUser;
user.sendEmailVerification().then(function() {
// Email sent.
}).catch(function(error) {
// An error happened.
});
This will send an email to your user containing a verification link. Thereafter you can use emailVerified property to check if user has verified their email to restrict access to data like:
if (user.emailVerified) {
// process
} else {
alert("Please verify your email")
}
In you case, the code would look like:
const { email, password, name } = this.state;
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(async (result) => {
await firebase.firestore().collection("Users")
.doc(firebase.auth().currentUser.uid)
.set({
name,
email
})
await user.sendEmailVerification()
console.log("Verification Email Sent")
})
I'm working on a react native project and I've came to a part where initially I implemented google sign in my project using react-native-google-signin and later on Facebook sign in using react-native-fbsdk packages with the help of firebase and both worked like a charm "individually".
The Problem
Let's say the user logged in using google account and it worked but later logged in using Facebook with the same account (I'm allowing only one email per user in firebase), I get an error
auth/account-exists-with-different-credentials
I want the user to be able to login using Facebook from the login screen or to be more specific to link his account from the login screen.
What have I tried?
I searched online and found some answers and got up with this solution or piece of code:
facebookSignin: async () => {
const result = await LoginManager.logInWithPermissions([
'public_profile',
'email',
]);
if (result.isCancelled) {
alert('User cancelled the login process');
this.setState({loginInProcess: false});
}
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
alert('Something went wrong obtaining access token');
this.setState({loginInProcess: false});
}
const facebookCredential = auth.FacebookAuthProvider.credential(
data.accessToken,
);
await auth()
.signInWithCredential(facebookCredential)
// The problem starts here from the catch block
.catch((error) => {
if (
error.code === 'auth/account-exists-with-different-credential'
) {
var pendingCred = error.credential;
var email = error.email;
auth()
.fetchSignInMethodsForEmail(email)
.then(async (methods) => {
if (methods[0] === 'google.com') {
const {idToken} = await GoogleSignin.signIn();
const googleCredential = auth.GoogleAuthProvider.credential(
idToken,
);
auth()
.signInWithCredential(googleCredential)
.then((user) => {
user.linkWithCredential(pendingCred);
})
.catch((error) => console.log(error));
}
});
}
});
}
This code implements a function when triggered, if there is no user with the same email, it proceeds normally, however if there is an error (mentioned above), it will grant the user with a list of google accounts that are present in the user phone (google thing) and when he chooses his account (linked with google account) it doesn't work. The email isn't linked.
To be more specific, I would like somehow to not grant the user with all his google accounts but only with the email to be linked var email = error.email; (in the code snippet above) and for the Facebook provider to be linked successfully.
After a little of hard work, I've managed to make it work in react native and I'm gonna leave the answer here for peeps who are facing the same issue. Be ware that I used react-native-prompt-android to ask the user for confirming his password when trying to link with Facebook.
The user tries to sign with Facebook and gets this error:
auth/account-exists-with-different-credentials
This is how I handled it:
.catch((error) => {
// Catching the error
if (
error.code === 'auth/account-exists-with-different-credential'
) {
const _responseInfoCallback = (error, result) => {
if (error) {
alert('Error fetching data: ' + error.toString());
} else {
setEmail(result.email);
}
};
// Getting the email address instead of error.email from Facebook
const profileRequest = new GraphRequest(
'/me?fields=email',
null,
_responseInfoCallback,
);
new GraphRequestManager().addRequest(profileRequest).start();
if (email) {
auth()
.fetchSignInMethodsForEmail(email)
.then(async (methods) => {
// Checking the method
if (methods[0] === 'password') {
// Prompting the user to confirm/input his password for linking
const AsyncAlert = () => {
return new Promise((resolve, reject) => {
prompt(
'Password Confirmation',
'The email address is already linked with password account. Enter your password to process',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Continue',
onPress: (password) =>
resolve(setPassword(password)),
},
],
{
type: 'secure-text',
cancelable: false,
placeholder: 'Password',
},
);
});
};
// Here the linking goes
await AsyncAlert().then(async () => {
await auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
return auth().currentUser.linkWithCredential(
facebookCredential,
);
})
.catch(() => alert('Something went wrong'));
});
} else if (methods[0] === 'google.com') {
const {idToken} = await GoogleSignin.signIn(email);
const googleCredential = auth.GoogleAuthProvider.credential(
idToken,
);
await auth()
.signInWithCredential(googleCredential)
.then(() => {
return auth().currentUser.linkWithCredential(
facebookCredential,
);
});
}
});
} else {
alert('Something went wrong');
}
}
});
I'm trying to implement auth guards in Nuxt with Firebase Auth, but I keep running in to problems. At the moment I'm able to login, but the correct page isn't loaded after login, after login the user should be redirected to the '/profile-overview' page but that doesn't happen. When I navigate away from the 'profile' page to another page and then go back I do automatically go to the 'profile-overview' page. So the login works, there is just something wrong with the navigation / refresh of the page after login. Also when I refresh the page the user is logged out again, I would except the user to still be logged in then?
My code so far:
Page:
loginGoogle () {
this.$store.dispatch('signInWithGoogle').then(() => {
console.log('reload')
location.reload()
//window.location.reload(true)
}).catch((e) => {
this.title = 'Google login failed'
this.message =
"Something went wrong, please try again later. If this problem keeps happening please contact: jonas#co-house.be " + "Error: " + e.message;
this.dialog = true;
})
},
Middleware:
export default function ({ store, redirect, route }) {
console.log('user state' + store.state.user)
console.log('route ' + route.name)
store.state.user != null && route.name == 'profile' ? redirect('/profile-overview') : ''
store.state.user == null && isAdminRoute(route) ? redirect('/profile') : ''
}
function isAdminRoute(route) {
if (route.matched.some(record => record.path == '/profile-overview')) {
return true
}
}
Plugin:
import { auth } from '#/services/fireInit.js'
export default context => {
const { store } = context
return new Promise((resolve, reject) => {
auth.onAuthStateChanged(user => {
if (user) {
return resolve(store.commit('setUser', user))
}
return resolve()
})
})
}
Store (function to login only:
signInWithGoogle ({ commit }) {
return new Promise((resolve, reject) => {
auth.signInWithPopup(GoogleProvider).then((result) => {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
return resolve(store.commit(state.user, result.user))
// ...
}).catch((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;
// ...
})
})
},
Does anyone have any idea what I could be doing wrong, or some documentation / tutorial I could read?
Thanks in advance.
You need to init your user on server in nuxtServerInit. See this repo for example implementation https://github.com/davidroyer/nuxt-ssr-firebase-auth.v2
I'm authenticating users iva JWT Auth on WP Rest API from a React Native app, so, something like that:
export function userAuth(username, password) {
return dispatch => {
dispatch(requestLogin());
const appAuth = new FormData();
appAuth.append('username', username);
appAuth.append('password', password);
return fetch(wp.jwtEndpoint, {
method: 'POST',
body: appAuth
})
.then(function(res) {
return res.json();
})
.then(function(body) {
if(body.token){
getUserDataFromUsername(body.user_nicename, body.token, dispatch);
return dispatch(userSuccessLogin(body));
}
else {
return dispatch(userFailLogin(body));
}
});
}
}
The response for this request is:
{ token: 'eyJ0eXAiOiJKXXXQ',
user_email: 'supertest#gmail.com',
user_nicename: 'super-test-avatar',
user_display_name: 'TEST TEST' }
My issue is: as I can't get user ID from there, how can I retrieve user data form a request like https://www.wp.com/wp-json/wp/v2/users/${userId}?
I tried using https://github.com/dest81/wp-api-get-user-by-username, which would allow me to do that based on username, but its endpoints goes to 404, so I think it's outdated.