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

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?

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

Firebase + stripe + react native informing the client that the firebase function has finished

I am implementing a payment system with Stripe extension for firebase in a react native. However, i do not know how to behave in the following situation:
I write initial information for the checkout_session when the user wants to proceed to checkout:
const initializeCheckout = () => {
//write initial payment data
const writePaymentDetails = async () => {
await setDoc(doc(getFirestore(), 'customers', getAuth().currentUser.uid, 'checkout_sessions', getAuth().currentUser.uid),{
client: 'mobile',
mode: 'payment',
amount: subTotal,
currency: 'chf',
});
}
writePaymentDetails();
navigation.navigate('Checkout');
}
After that, a stripe extension in firebase adds all the additional information (ephemeral keys, stripe customer key etc.) to the checkout_session document.
After additional data is written, i want to navigate to the checkout page and then initialize and open paymentSheet in react native as it is indicated in the official stripe tutorial
The checkout screen i implemented:
export default function CheckoutScreen() {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
const [loading, setLoading] = useState(false);
const fetchPaymentSheetParams = async () => {
console.log('still works after calling fetchPaymentSheetParams');
const checkoutSessionDoc = await getDoc(doc(getFirestore(), 'customers', getAuth().currentUser.uid, 'checkout_sessions', getAuth().currentUser.uid));
const paymentIntent = checkoutSessionDoc.data().paymentIntentClientSecret;
const ephemeralKey = checkoutSessionDoc.data().ephemeralKeySecret;
const customer = checkoutSessionDoc.data().customer;
console.log(paymentIntent, ephemeralKey, customer);
return{
paymentIntent: paymentIntent,
ephemeralKey,
customer,
};
};
const initializePaymentSheet = async () => {
const {
paymentIntent,
ephemeralKey,
customer,
} = await fetchPaymentSheetParams();
const { error } = await initPaymentSheet({
customerId: customer,
customerEphemeralKeySecret: ephemeralKey,
paymentIntentClientSecret: paymentIntent,
allowsDelayedPaymentMethods: false,
});
if (!error) {
setLoading(true);
}
};
const openPaymentSheet = async () => {
const { error } = await presentPaymentSheet();
if (error) {
Alert.alert(`Error code: ${error.code}`, error.message);
} else {
Alert.alert('Success', 'Your order is confirmed!');
}
};
useEffect(() => {
console.log('Payment sheet is being initialized');
initializePaymentSheet();
}, []);
return (
<View style={{flex: 1, justifyContent: 'center'}}>
<Button
disabled={loading}
title="Checkout"
onPress={openPaymentSheet}
/>
</View>
);
}
However, i don't know how to wait until the firebase function ends in step 2 before moving to the next step. Now, if i navigate to the checkout screen just after writing the initial data and try to read an ephemeral key, stripe customer key and payment intent, they are undefined.
So, my question is how to make the transition correctly so that the additional information is not undefined?
i have solved this in a cumbersome(most probably) way:
I have defined a snapshot listener that checks if the fields customer, customerEphemeralKeySecret and paymentIntentClientSecret are defined and only then starts initializing the payment sheet.

How do I create custom tokens in Firebase using React?

I am trying to create a custom a custom token to log users in with their username. I've been through some of the documentation https://firebase.google.com/docs/auth/admin/create-custom-tokens#web, which was linked to me via How to provide user login with a username and NOT an email?, and I have seen that I need to add
Create custom tokens using the Firebase Admin SDK
and
Sign in using custom tokens on clients
At the moment I can kinda see what needs to be included based on the documentation, but I am unsure as to where this would go in the source code. Where do I add the code from the documentation? This is the source code for the userUser.js file, in case it helps.
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import firebase from "firebase/app";
import "firebase/auth";
import initFirebase from "../../config";
import {
removeUserCookie,
setUserCookie,
getUserFromCookie,
} from "./userCookie";
initFirebase();
export const mapUserData = async (user) => {
const { uid, email } = user;
const token = await user.getIdToken(true);
return {
id: uid,
email,
token,
};
};
const useUser = () => {
const [user, setUser] = useState();
const router = useRouter();
// this is most likely where the custom token for
// username goes
const logout = async () => {
return firebase
.auth()
.signOut()
.then(() => {
router.push("/");
})
.catch((e) => {
console.error(e);
});
};
useEffect(() => {
const cancelAuthListener = firebase
.auth()
.onIdTokenChanged(async (userToken) => {
if (userToken) {
const userData = await mapUserData(userToken);
setUserCookie(userData);
setUser(userData);
} else {
removeUserCookie();
setUser();
}
});
const userFromCookie = getUserFromCookie();
if (!userFromCookie) {
return;
}
setUser(userFromCookie);
return () => cancelAuthListener;
}, []);
return { user, logout };
};
export { useUser };
Any help would be greatly appreciated.
You can only use the admin sdk in a server environment (like in Firebase Functions or some other server) - you can't use it in the client environment where you're using React. Conceptually, the way this works is:
User enters a username and password in your client app
Client app sends the username and password to your server
Server checks the username and password and, if correct, creates a custom token using the admin SDK and sends that back to the client app
Client app uses that custom token to sign into Firebase
So it would look something like this (note - I don't handle any errors here but you'll want to):
// client.js
const sendToServer = (username, password) => {
// Step 1 - client sends the username/password to the cloud function
return axios.post(`${myCloudFunctionUrl}/login`, {
username,
password
}).then((response) => {
// Step 5 - the client logs the user in with the custom token
return firebase.auth().signInWithCustomToken(response.data.token)
}).then(() => {
// Step 6 - the user is now logged in and redirected to the dashboard
router.push("/dashboard")
})
}
// server.js (using Firebase Functions, but use whatever back end you want)
exports.login = functions.https.onRequest((req, res) => {
const {username, password} = req.body
// Step 2 - function verifies the username and password and gets the user's uid for the custom token
return verifyUserInDatabase(username, password).then((uid) => {
// Step 3 - the server creates a custom token
return admin.auth().createCustomToken(uid)
}).then((token) => {
// Step 4 - the server sends the token back in its response
res.json({ token })
})
})

Firebase Facebook login popup does not let you pick different account

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.

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