Firebase Facebook login popup does not let you pick different account - javascript

I'm working on adding Facebook and Google login to an app. So far so good with the Google side of things, ot works but not the Facebook. I found out that having two emails linked to different accounts causes a problem so I handled it in the catch() of loginWithProvider().
The problem I have is that I cannot get the user to pick another account. The Facebook login popup opens, detects that there is another account with that email and opens the Google popup. I want to let the user choose the Facebook account to log in with. I thought the prompt: "select_account" would fix it but it does not.
Here is my code:
const loginWithProvider = (provider, onSuccess) => async dispatch => {
Firebase.auth()
.signInWithPopup(provider)
.then(result => {
// register the user in my backend here
})
.catch(({ code, email }) => {
if (code === "auth/account-exists-with-different-credential") {
Firebase.auth()
.fetchSignInMethodsForEmail(email)
.then(providers => {
if (providers.includes("google.com")) {
dispatch(
loginWithGoogle(onSuccess, {
login_hint: email
})
);
}
});
}
})
.then(() => isFunction(onSuccess) && onSuccess());
};
export const loginWithFacebook = (onSuccess, parameters) => async dispatch => {
const provider = new Firebase.auth.FacebookAuthProvider();
provider.setCustomParameters({ ...parameters, prompt: "select_account" });
dispatch(loginWithProvider(provider, onSuccess));
};
export const loginWithGoogle = (onSuccess, parameters) => async dispatch => {
const provider = new Firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({ ...parameters, prompt: "select_account" });
dispatch(loginWithProvider(provider, onSuccess));
};
Any ideas?
Edit: After some research it turns out that prompt: "select_account" is valid for the Google popup but not for the Facebook one. I'm struggling to find what do I need to use for Facebook's.

Related

How to refresh an IdToken to acquire a custom claim after a user signs up with Firebase

I'm trying to use Firebase custom claims to protect content for my users, but the first time a user signs up and is redirected to /protectedpage, they cannot view the page because their claim is not set. If they log out and log back in, everything works properly.
Signup Flow
User signs up with email and password
A user document is created in a users collection in Firestore
The user is redirected to /protectedpage
Creation of the user document triggers a cloud function which assigns the custom claim role=A or role=B depending on the information in the user document.
In Javascript (React), it looks like this
Client side
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Add a new document in collection "users"
setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Send email verification
sendEmailVerification(userCredential.user)
.then(() => {
// Redirect to home page
router.push('/protectedpage');
})
.catch((error) => {
console.log("Error sending email verification", error.message);
});
})
.catch((error) => {
setFormError(error.message);
})
Server side
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// This function runs when a document is created in
// the users collection
exports.createUser = functions.firestore
.document('users/{userId}')
.onCreate(async (snap, context) => {
// Get an object representing the document
const doc = snap.data()
const userId = context.params.userId;
// Declare customClaims
let customClaims = {};
// Assign user role
if (doc.account_type == 'A') {
customClaims["role"] = "A"
} else if (doc.account_type == 'B') {
customClaims["role"] = "B"
} else {
functions.logger.info('A role could not be assigned to user:', doc)
response.send('Error: A role could not be assigned')
}
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(userId, customClaims);
} catch (error) {
functions.logger.info(error);
}
return "OK"
})
By the time the user gets to /protectedpage, his JWT does not have the custom claim.
Authorization
My authorization code is using a React context manager, and looks like this
import { createContext, useContext, useEffect, useState } from 'react'
import { onAuthStateChanged, signOut as authSignOut } from 'firebase/auth'
import { auth } from './firebase'
export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const clear = () => {
setAuthUser(null)
setIsLoading(false)
}
const authStateChanged = async (user) => {
setIsLoading(true)
if (!user) {
clear()
return
}
// Use getIdTokenResult() to fetch the custom claims
user.getIdTokenResult()
.then((idTokenResult) => {
console.log("idTokenResult", idTokenResult)
setAuthUser({
uid: user.uid,
email: user.email,
role: idTokenResult.claims.role,
})
setIsLoading(false)
})
.catch((error) => {
console.log(error)
})
}
const signOut = () => authSignOut(auth).then(clear)
// Listen for Firebase Auth state change
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, authStateChanged)
return () => unsubscribe()
}, [])
return {
authUser,
isLoading,
signOut,
}
}
const AuthUserContext = createContext({
authUser: null,
isLoading: true,
signOut: async () => {},
})
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth()
return (
<AuthUserContext.Provider value={auth}>{children}</AuthUserContext.Provider>
)
}
export const useAuth = () => useContext(AuthUserContext)
If I change user.getIdTokenResult() to user.getIdTokenResult(true), the user no longer has to sign out and sign back in to access the custom claim BUT
They need to manually refresh the page to acquire the custom claim
I think this is bad, as it's going to forcibly refresh the token on every page load ??
The Firebase docs seem to address this problem with some trickery involving "metadataRef" but I don't understand it exactly, as I think it's related to the Realtime database whereas I'm using Firestore.
Finally got this to work. Two things were tripping me up.
router.push('/protectedpage') doesn't do a hard refresh. I changed this to window.location.replace('/protectedpage')
Instead of assigning the custom claim on creation of the user record, I wrote a cloud function to do it. After my user is created, I call this function. After I get the response, then I redirect the user to /protectedpage
My cloud function looks like this
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// IMPORTANT:
// Note the distinction between onCall and onRequest
// With onCall, authentication / user information is automatically added to the request.
// https://stackoverflow.com/questions/51066434/firebase-cloud-functions-difference-between-onrequest-and-oncall
// https://firebase.google.com/docs/functions/callable
// Function to set a user's role as either "A" or "B"
exports.setRole = functions.https.onCall((data, context) => {
// Check that the user is authenticated.
if (!context.auth) {
// Throw an HttpsError so that the client gets the error details.
// List of error codes: https://firebase.google.com/docs/reference/node/firebase.functions#functionserrorcode
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// Confirm that the function contains a role
if (!data.hasOwnProperty("role")) {
throw new functions.https.HttpsError(
'failed-precondition',
"The function data must contain a 'role'"
);
}
// Confirm that role is either A or B
if (data.role !== "A" && data.role !== "B") {
throw new functions.https.HttpsError(
'failed-precondition',
"'role' must be set to either 'A' or 'B'"
);
}
// Confirm that the user doesn't already have a role
if (context.auth.token.role) {
throw new functions.https.HttpsError(
'failed-precondition',
"The user's role has already been set"
);
}
// Assign the role
// IMPORTANT:
// We need to return the promise! The promise returns the response. This way, on the client,
// we can wait for the promise to get resolved before moving onto the next step.
return getAuth().setCustomUserClaims(context.auth.uid, { role: data.role })
.then(() => {
return "OK"
})
.catch((error) => {
throw new functions.https.HttpsError(
'internal',
'Error setting custom user claim'
);
})
})
and I call it from the client like this
// Handle form submission
const onSubmit = (formValues) => {
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Send email verification
sendEmailVerification(user);
// Add a new document in collection "users"
const promise1 = setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Set the user role (custom claim)
// Then force refresh the user token (JWT)
const setRole = httpsCallable(functions, 'setRole');
const promise2 = setRole({ role: formValues.account_type })
.then(() => user.getIdTokenResult(true));
// When the user document has been created and the role has been set,
// redirect the user
// IMPORTANT: router.push() doesn't work for this!
Promise.all([promise1, promise2]).then((values) => {
window.location.replace('/protectedpage');
})
})
.catch((error) => {
setFormError(error.message);
})
}

How do I validate user in reCAPTCHA v3 in react.js?

I am using the library "react-google-recaptcha-v3" for reCAPTCHA in React.
I have a SignUp component, which essentially is something along the lines of a form where users can add their credentials and then create an account.
The function looks like this:
const SignUp = () => {
...
const { executeRecaptcha } = useGoogleReCaptcha();
const handleReCaptchaVerify = useCallback(async () => {
if (!executeRecaptcha) {
console.log("Execute recaptcha not yet available");
return;
}
const token = await executeRecaptcha();
console.log(token);
});
...
return (
...
<div
onClick={() => {
handleReCaptchaVerify().then(() => {
createUserEmailPassword(email, password, username);
});
}}
className="auth-sign-button"
>
Create a new account
</div>
...
);
};
As you can see the function handleReCaptchaVerify() is being called once the user tries to create a new account. This function provides me with a token, as the SignUp component is later rendered from within a component.
How do I, validate the user once I receive the token, from the handleReCaptchaVerify() function?

UserContext only works when manually navigating to a page, not when it is automatically done

This is part of my code. The login function I use in an onClick button:
const {user, setUser} = useContext(UserContext)
const login = () => {
const errors = validateForm(fields);
setErrors(errors);
if (!errors.length) {
const username = fields.find((field) => field.id === 'username').input.state.value;
const password = fields.find((field) => field.id == 'password').input.state.value;
setUser(username);
APIService.LoginUser({username, password})
}
}
It basically checks in the validateForm(fields) whether the user put something in the form. If there are no errors there, then it sets the user variable to be whatever the user inputted as the username. And that user I hope to be made available to other pages so the app knows who is logged in.
This is the APIService.LoginUser() code:
export default class APIService {
static LoginUser(username, password) {
return fetch(`http://localhost:5000/userlookup`, {
'method':'POST',
mode: 'cors',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(username, password)
})
.then(resp => window.location.href = "profile")
.catch(error => console.log('error time!', username, password))
}
The idea is that once the user is logged in successfully, the site will go straight to the "profile" page. On the profile page I have a simple greeting:
const Profile = () => {
const {user, setUser} = useContext(UserContext);
return (
<IonPage>
<IonContent>
<h1>
Hi, {user}
</h1>
</IonContent>
</IonPage>
)
};
export default Profile;
(from the tags, you can see that this is an ionic app, but I believe that is irrelevant)
My problem is that when I try to log in using the above code, after I get redirected to the profile page the greeting does not update with the username of the user that is logged in. However when I comment out this section of the LoginUser function:
.then(resp => window.location.href = "profile")
.catch(error => console.log('error time!', username, password))
and manually navigate to the profile page, it works correctly! It greets the logged in user! Is this an async problem? My theory is that when it automatically redirects to the profile page it does it too fast for the UseContext to tell it what to do? Is this theory true? Is it something else? How do I troubleshoot this?
Thanks
window.location.href = "profile"
that is not the correct way to navigate to different pages in Ionic Framework
const history = useHistory()
and then to go to another route
history.push("/profile")
See documentation - https://ionicframework.com/docs/react/navigation#ionreactrouter

Firebase - Change displayName for Facebook provider

I'm fairly new to the Firebase ecosystem, so I hope I'm not asking something too basic.
I'm using the firebase-js-sdk along with an e-mail + password registration. When the user signs up using an e-mail I prompt them to select their username and I store it using the user.updateProfile() method. This works fine, as the next time I call firebase.auth().currentUser I see the displayName property containing the updated value.
As for facebook, I'm using the react-native-fbsdk, and I authenticate the user using the following function:
const fbLogin = () => {
return new Promise((resolve, reject) => {
LoginManager
.logInWithReadPermissions(['public_profile', 'email'])
.then((result) => {
if (result.isCancelled) {
console.log('login cancelled');
} else {
AccessToken
.getCurrentAccessToken()
.then((data) => {
const credentials = firebase.auth.FacebookAuthProvider.credential(data.accessToken);
firebase
.auth().signInWithCredential(credentials)
.then((res) => resolve(res))
.catch((err) => reject(err));
})
}
}).catch(err => reject(err));
});
}
Once I store the user's data on Firebase I ask him to choose an username and I update the displayName following the same steps of the e-mail authentication. This seems to work too because if I call firebase.auth().currentUser I see the updated displayName. The only problem is when I reload the app the displayName is back to the facebook name.
My questions are:
Is it possible to override the displayName provided by Facebook?
If so, is this the correct approach to do so?
Thanks in advance to anyone that will help :)

Facebook login in React Native

I am developing an app in React Native and I want to implement logging in with Facebook.
I have an API in Node.js where I handle the logic for users to log in, etc.
I use passport.js to let users log in with either Facebook or traditional Email.
I am opening an URL in my API with SafariView which is just a regular "WebView" directly in my app.
I have tried using the following code:
class FacebookButton extends Component {
componentDidMount() {
// Add event listener to handle OAuthLogin:// URLs
Linking.addEventListener('url', this.handleOpenURL);
// Launched from an external URL
Linking.getInitialURL().then((url) => {
if (url) {
this.handleOpenURL({ url });
}
});
}
componentWillUnmount() {
Linking.removeEventListener('url', this.handleOpenURL);
}
handleOpenURL({ url }) {
// Extract stringified user string out of the URL
const [, user_string] = url.match(/user=([^#]+)/);
this.setState({
// Decode the user string and parse it into JSON
user: JSON.parse(decodeURI(user_string))
});
if (Platform.OS === 'ios') {
SafariView.dismiss();
}
}
openURL(url) {
if (Platform.OS === 'ios') {
SafariView.show({
url: url,
fromBottom: true,
});
} else {
Linking.openURL(url);
}
}
render() {
return (
<Button
onPress={() => this.openURL('https://mywebsite.com/api/auth/facebook')}
title='Continue with Facebook'
...
so I guess I will have to do the authentication on URL https://mywebsite.com/api/auth/facebook and then send the user to an url that looks something like OAuthLogin://..., but I am not entirely sure how to use it.
Can anyone help me move in the right direction?
import { LoginManager, AccessToken } from 'react-native-fbsdk'; // add this file using npm i react-native-fbsdk
Create function
const onFacebookButtonPress = async () => {
// Attempt login with permissions
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 userInfo = await AccessToken.getCurrentAccessToken();
if (!userInfo) {
throw 'Something went wrong obtaining access token';
}
console.log('user info login', userInfo)
// Create a Firebase credential with the AccessToken
const facebookCredential = auth.FacebookAuthProvider.credential(userInfo.accessToken);
setGoogleToken(userInfo.accessToken)
// Sign-in the user with the credential
return auth().signInWithCredential(facebookCredential)
.then(() => {
//Once the user creation has happened successfully, we can add the currentUser into firestore
//with the appropriate details.
console.log('current User ####', auth().currentUser);
var name = auth().currentUser.displayName
var mSplit = name.split(' ');
console.log("mSplit ",mSplit);
let mUserDataFacebook = {
user_registration_email: auth().currentUser.email,
user_registration_first_name: mSplit[0],
user_registration_last_name: mSplit[1],
registration_type: 'facebook',
user_registration_role: "Transporter",
token: userInfo.accessToken,
user_image : auth().currentUser.photoURL,
};
console.log('mUserDataFacebook',mUserDataFacebook)
LoginWithGoogleFacebook(mUserDataFacebook) /// Call here your API
firestore().collection('users').doc(auth().currentUser.uid) //// here you can add facebook login details to your firebase authentication.
.set({
fname: mSplit[0],
lname: mSplit[1],
email: auth().currentUser.email,
createdAt: firestore.Timestamp.fromDate(new Date()),
userImg: auth().currentUser.photoURL,
})
//ensure we catch any errors at this stage to advise us if something does go wrong
.catch(error => {
console.log('Something went wrong with added user to firestore: ', error);
})
})
}
Call this function on button press onFacebookButtonPress()
For android need to setup and add facebook id in
android/app/src/main/res/values/strings.xml file
add these two lines.
YOUR_FACEBOOK_ID
fbYOUR_FACEBOOK_ID //Don't remove fb in this string value
/////////////add this code in AndroidMainfest.xml file
//////////This code add in MainApplication.java file
import com.facebook.FacebookSdk;
import com.facebook.appevents.AppEventsLogger;
/////////add code build.gradle file
implementation 'com.facebook.android:facebook-android-sdk:[5,6)'

Categories