no way to catch Firebase: Error (auth/internal-error) - reactjs

I'm using React js with firebase auth to authenticate user on a web app.
using createUserWithEmailAndPassword and signInWithEmailAndPassword to create/sign-in user, and use onAuthStateChanged to get the current user.
let's say user has created account/signed in and then refreshes browser page while connected to internet, in this case as soon as the app runs, user gets subscribed again by onAuthStateChanged which is in a useEffect hook, until now everything is ok, the problem comes when the user has create account and goes offline and then refreshes the page, so now app throws this error:
I tried to catch error like below code, but no result:
useEffect(() => {
let unsubscribe
try{
unsubscribe = firebaseApp.auth().onAuthStateChanged( userInfo => {
if(userInfo){
const {uid:userId,email,displayName}=userInfo.multiFactor.user
dispatch( setUser({userId,email,displayName}))
}
})
}catch(err){
console.log(err);
setError('You may are offline or have bad internet')
}
return unsubscribe
}, [])
I also tried the way that firebase doc has suggested for errors like below, but yet no result:
useEffect(() => {
const unsubscribe = firebaseApp.auth().onAuthStateChanged( userInfo => {
if(userInfo){
const {uid:userId,email,displayName}=userInfo.multiFactor.user
dispatch( setUser({userId,email,displayName}))
}
},err=>console.log(err))
return unsubscribe
}, [])
I've read firebase doc about (auth/internal-error) but it was not clear on how to catch and handle it.
Any help?

The onAuthStateChanged observer is primarily used to notify the observer code when a user becomes signed in or signed out for any reason. It typically does not deal with errors.
If you want to know if the sign-in process failed, you should check for errors in the code that initiated the sign-in, which you aren't showing here. Somewhere, you must be calling one of the sign-in methods, such as signInWithEmailAndPassword, which does generate errors. Or perhaps you are using FirebaseUI Auth to manage sign-ins, which has its own way of dealing with errors.
However, as you can see in the API documentation for onAuthStateChanged, you can pass an optional second argument which is a function that yields an error, but that is atypical to use, and it's not clear from the API docs how it works. I suggest sticking to handling errors in the code that performs the sign-in as you see in the code in the documentation.

Related

Next.js production: CANNOT READ DATA FROM FIRESTORE

I have a next.js application in which I have a real-time chat feature.
A get() request is supposed to be sent to firestore to check if chat between user A and user B already exists. If it does not already exist, a new chat between A and B is created
Problem:
The get() request to firestore always returns an empty value. So a new chat room between user A and user B is always created.
Since a new chat room is created successfully, I can write to firestore but cannot read data.
the code:
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const [chats, setChats] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await dbs
.collection("chats")
.where("users", "array-contains", "some#email")
.get();
setChats(response);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
console.log("chat is", chats);
return <div></div>;
}
export default Test;
What I have attempted and discovered:
I deployed an entirely new next.js app (let's call it app B) and tried to get data from the same firestore project with the same code above and it works. So my firestore configuration is fine, the code is fine, and there's nothing wrong with the deployment server.
Since there's nothing wrong with firestore or the deployment server or the code, I think something is wrong with the next.js app itself and the dependencies surrounding it. I don't know why it works in app B and not in app A.
Note, in App A:
Everything works fine in development mode, I can get the data.
I'm also using mongoDB and other third-party APIs in this app and I can successfully read and write data to these APIs. Only the get() request to firestore is not working. which made me think the problem must be with my firestore config but app B proves that's not the case.
I'm using google OAuth with mongoDB and next-auth to authenticate my users in app A. In app B I'm only using google OAuth without the DB.
I could show the code for both app A and app B but they are exactly the same. The code under the _app.js file is the same, the way I structure it is also the same. In both app A and app B, I called the get() request to firestore from a Test.js component in a test.js page but the get() request only returns value in app B and always returns null in app A.
So basically the only difference is app A is an actual app with plenty of dependencies, libraries, and files. app B is just a test app.
My question now is:
Could the read operation to firestore be affected by other dependencies or npm libraries in the project? or could it be affected by using another DB to get the auth session context?
Why is the write operation successful but not the read?
Why does it work in dev mode but not in prod mode? is there any difference between retrieving data from firestore in dev mode vs prod mode?
As explained yesterday, you aren't paying close enough attention to the intermediate states properly and it is likely that NextJS is tripping up because of it.
On the first render of your current code, chats is an empty array. Even so, once the promise fulfills, you update chats to a QuerySnapshot object, not an array.
import { useEffect, useState } from "react";
import { dbs } from "../utils/firebase";
function Test() {
const currentUserEmail = "some#email"; // get from wherever
const [chatsInfo, setChatsInfo] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
let unsubscribed = false;
const fetchData = async () => {
return dbs
.collection("chats")
.where("users", "array-contains", currentUserEmail)
.get();
};
fetchData()
.then(() => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "loaded",
data: response.docs, // QueryDocumentSnapshot[]
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // do nothing
setChatsInfo({
status: "error",
data: null,
error: err
});
});
};
// return a unsubcribe callback that makes sure setChatsInfo
// isn't called when the component is unmounted or is out of date ​
​return () => unsubscribed = true;
​ }, [currentUserEmail]); // rerun if user email changes
​
​ const { status, data: chats, error } = chatsInfo;
console.log(JSON.parse(JSON.stringify({ status, chats, error }))); // crude object copy
switch (status) {
case "loading":
return (<div>Loading...</div>);
case "error":
return (
<div class="error">
Failed to load data: {error.code || error.message}
</div>
);
}
// if here, status is "loaded"
if (chats.length === 0) {
// You could also show a "Message Someone" call-to-action button here
return (
<div class="error">
No chat groups found.
</div>
);
}
// stamp out list of chats
return (<>
{chats.map((chatDoc) => {
return (
<div key={chatDoc.id}>
{JSON.stringify(chatDoc.data())}
</div>
);
})}
</>);
}
export default Test;
Notes:
A decent chunk of the above code can be eliminated by using an implementation of useAsyncEffect like #react-hook/async and use-async-effect. These will handle the intermediate states for you like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.
I highly recommend not using email addresses in their raw form for user IDs. With the current structure of your database, anyone can come along and rip all the emails out and start spamming them.
Each user should have some private identifier that doesn't reveal sensitive information about that user. This could be a Firebase Authentication User ID, the user's email address hashed using md5 (which also allows you to use Gravatar for user profile pictures) or some other ID like a username. Once you have such a user ID, you can use the approach outlined in this thread for handling messages between users.

How to redirect an user to a specific webpage after Google Sign In with Redirect in Firebase?

The nature of the sign-in flow with Google/Facebook is that after we login in the redirected page of Google, it comes back to our website's sign-in page.
The following code runs when the Google/Facebook login button is clicked:
fire.auth().signInWithRedirect(provider);
So, my current approach is that I check the Firebase user object using the onAuthStateChanged() function. If the user state is populated, I render a component, else if it is null, I render the component.
{user ? (
<Home />
) : (
<Signup />
)}
But the problem is that after logging in using Google or Facebook, the component is showing for some time (maybe 1-2 secs) and then rendering the component.
I want to render the component immediately after I login using Google redirect. What should I do?
google and facebook login system are asynchronous in nature so you shoud you async await method inside you code.
You should show a full loading state in the login screen when clicking on the signin button
So when the authentication starts show the user a loader,
and if fails stop the loader
you could do something like
if (authUser === undefined || isLoading) {
return <AuthLoader />;
}
return <LoginComponentContents/>
I would recommend to use for all authentication methods the onAuthStateChanged listener in auth. That way it doesn't matter what method you use. It will give you the user if someon is logged in and null if not. The code looks like this:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
Use that listener to define the state of your auth (if a user is signed in or not). You can also persiste the data in your state menegement to awoid flickering on page reloads. You can use the same method for email/password login. That way you have a single and simple solution for all Firebasse authentication methods.
For the time you await the Google or other provider redirect login I would recommend to set a loading flag and show a circular loading indicator. The login could always fail. I would not recommend to show pages that require a signed in user untill you get the response from authStateChanged as expected.
For Firebase v9 and above:
If using an AuthContext file, you can define your signInWithRedirect and getRedirectResult functions. Then, import those functions into your Signup page.
AuthContext:
//Log in user with provider. Redirects away from your page.
async function loginGoogle() {
await signInWithRedirect(auth, provider);
return
}
//Checks for provider login result, then navigates
async function getRedirect(){
const result = await getRedirectResult(auth);
if (result) {
navigate('/')
}
}
In your Signup page, call the login function on button click
//Sign in with Google.
async function handleGoogleLogin(e) {
e.preventDefault()
try {
setError('')
await loginGoogle()
} catch(error) {
setError('Failed to log in')
console.log(error)
}
}
Then, just place the getRedirect() function in your Signup component. This function will run when the page reloads from the google redirect, thus sending your user to the desired page.
//Checks for Google Login result.
getRedirect();
For me, it only worked when using this 2-step approach because when the provider redirect occurs, the async function appears to not finished as expected. So loginWithRedirect in one step, then getRedirectResult and navigate in a second step.

Handling authentication persistence in React (with Redux and Firebase)

In my React project, I've implemented authentication using Firebase. The workflow is as follows - The UID which is received upon signing in with Google OAuth is used to query the users in the firestore, and accordingly, the user is fetched. This user is then updated to the redux state by the appropriate handler functions. This is the implementation of the sign-in process. The setUser function does the task of updating the redux state
googleAuth = async () => {
firebase.auth().signInWithPopup(provider).then(async (res) => {
const uid = res.user.uid;
const userRef = firestore.collection('users').where('uid', '==', uid);
const userSnap = await userRef.get();
if(!userSnap.empty) {
const user = userSnap.docs[0].data();
this.props.setUser(user);
}
this.props.history.push('/');
}).catch(err => {
alert(err.message);
})
}
The presence of "currentUser" field in the user state of redux, as a result of the aforementioned setUser function, is the basis of opening the protected routes and displaying relevant user-related details in my project. This currentUser is persisted in the state using Redux Persist, so as long as I don't sign out, the localStorage has the "currentUser" object stored, thus meaning that I am signed in.
This implementation, however, causes a problem, that if I update the localStorage using the console and set the currentUser without actually logging in using my Google credentials, the app recognises me as logged in, and I am able to browse all protected routes. Thus, if someone copies some other user's details from his device's localStorage, then he/she can easily access their account and change/delete important information.
What is the correct way to implement authentication in React then, assuming that I do want login state to persist until the user does not sign out manually

How to wait for auth state to load on the client before fetching data

Recently I stumbled across the useAuth and useRequireAuth hooks: https://usehooks.com/useRequireAuth/. They are incredibly useful when it comes to client-side authentication. However, what's the correct way for waiting until auth data is available to fetch some other data? I've come up with the following code:
const Page = () => {
// makes sure user is authenticated but asynchronously, redirects if not authenticated, short screen-flash
useRequireAuth()
// actual user object in state, will be updated when firebase auth state changes
const user = useStoreState((state) => state.user.user);
if (!user) {
return <div>Loading</div>
}
useEffect(() => {
if (user) {
fetchSomeDataThatNeedsAuth();
}
}, [user]);
return (
<h1>Username is: {user.name}</h1>
)
}
Is this a "good" way to do it or can this be improved somehow? It feels very verbose and needs to be repeated for every component that needs auth.
This looks fine to me. The thing you could improve is that your useRequireAuth() could return the user, but that's up to you.
Additionally, you probably should check if user is defined before rendering user.name.

Firebase: return caught error from signInWithEmailAndPassword

I am using firebase with Email+Password auth provider. Correct credentials case works as expected. My problem is that if credentials are wrong, error is caught, but I don't know how to pass this information back.
I am connecting dispatch from redux store with mapDispatchToProps.
const mapDispatchToProps = (dispatch) => ({
startLogin: (email, password) => dispatch(startLogin(email, password))
});
Then I just call this action with credentials from my component and if undefined was returned (error thrown), I set errors.
startLogin(email, password).then((data) => {
if (!data) {
setErrors({ login: 'Wrong credentials' });
}
});
My action look like this.
export const startLogin = (email, password) => {
return firebase.auth().signInWithEmailAndPassword(email, password).catch((err) => {
console.log(err);
});
};
This works, but console shows POST 400 error which I would like to avoid.
You can't avoid getting that error logged into the console, because:
1) This HTTP request is how Firebase Authentication verifies the user credentials. So the library actually POSTs the credentials, and if they're not good then the server responds with that status.
And then...
2) Your browser decides to log bad requests to the log. Firebase wraps up the error for you to handle, but by this time the browser has already logged it into the console, something you can't prevent.
If you had control over the API you might like to always return a 200 code with some custom error handling logic, but in this case you should consider making peace with it :)

Resources