I'm using React Native/Firebase/Redux to build a simple login system. I am trying to work out how to capture errors that happen as a result of failed login attempts.
Here's my authscreen.js:
const [alertShowing, setAlertShowing] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
...
function handleLogin() {
const response = dispatch(login(email, password));
console.log(response);
}
actions.js:
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
return e;
}
};
};
My console.log(response) above correctly shows me the error message, but this obviously isn't very useful to users. And please note too that I can log in properly when using correct credentials.
What I really want to do in my handleLogin() is check if the response is an error, and if so, setlAlertShowing(true) and setAlertMessage to what I've gotten back from the useDispatch hook so that I may display it nicely to the user.
How should I go about this? TIA.
Firebase errors messages are designed for developers and not standard users friendly. The solution is to identify authentication error code and map with user-friendly messages.
list of error code https://firebase.google.com/docs/auth/admin/errors
You can use function like this to map authError to meaningful error messages.
function mapAuthCodeToMessage(authCode) {
switch (authCode) {
case "auth/invalid-password":
return "Password provided is not corrected";
case "auth/invalid-email":
return "Email provided is invalid";
// Many more authCode mapping here...
default:
return "";
}
}
and use it later
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
dispatch({type:"authError",message:mapAuthCodeToMessage(e.code)}));
}
};
};
Related
I'm a beginner in Next.js and Firebase. I was trying to make a log in system that has roles. I used Firebase Authentication and stored the account's roles on firestore. I connected the authentication account and firestore data by using the UID (from authentication) as the Firestore Document ID.
Ps. I'm not sure if this is the best way to do this but I am able to fetch data from a field by looking for the Document ID fetched using the UID.
What I need to happen is to get the role of the Account from the Firestore and use that for a function to push the correct page for that account type. The problem is, my pushToPage() fires first before my getData() gets the information it asks for from the firestore.
This is the LogIn function. I used auth.onAuthStateChanged to get the UID of the user.
var firebaseDocument = ' ';
var accountType = '';
var databaseRef = '';
function LogIn() {
signInWithEmailAndPassword(auth, email, password)
.then((response) => {
sessionStorage.setItem('Token', response.user.accessToken);
auth.onAuthStateChanged((user) => {
if (user) {
firebaseDocument = user.uid;
databaseRef = doc(database, 'users', firebaseDocument);
} else {
console.log('user logged out');
}
});
})
.catch((err) => {
console.log(err.code);
});
accountType = getData();
pushToPage(accountType);
}
This is the getData function where it fetches the account role
const getData = async () => {
try {
const docSnap = await getDoc(databaseRef);
if (docSnap.exists()) {
return docSnap.data().account_type;
} else {
console.log('Document does not exist');
}
} catch (e) {
console.log(e);
}
};
this is the pushToPage function that reads the var accountType value to decide on what page to go.
const pushToPage = (accountType) => {
if (accountType == 'management') {
router.push('/accounts/management');
} else if (accountType == 'dean') {
router.push('/accounts/deans');
} else {
router.push('/accounts/faculty');
}
};
Running this code does not only make my Program wait for the firebase response but also displays this error
Unhandled Runtime Error
FirebaseError: Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore
I didn't use collection() though.
Also, I searched for similar problems and I think I need to place a .then() function somewhere or somekind of Asynchronous Programming technique. Unfortunately, I struggle in understanding these.
I hope I can find help here. Thanks guys.
I don't think I need the .then() function anymore. I just called the pushToPage() inside the getData.
const getData = async () => {
try {
const docSnap = await getDoc(databaseRef);
if (docSnap.exists()) {
accountType = docSnap.data().account_type;
pushToPage(accountType);
} else {
console.log('Document does not exist');
}
} catch (e) {
console.log(e);
}
};
After a form submission using Redux, I am able to see the plain text password in the dev tools meta section. Is this safe? Am I doing something wrong when passing the password down to the reducer? How can I make this more secure?
So in my userSlice I am creating an Async Thunk that accepts user input then grabs the user from my server/database.
export const setUserAsync = createAsyncThunk(
'user/setUserAsync',
async (payload, { rejectWithValue }) => {
try {
const response = await axios.post('/auth/login', payload);
const { token, user } = response.data;
console.log(response);
localStorage.setItem('user', JSON.stringify(user));
localStorage.setItem('token', token);
return user;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
which works as intended. I am then calling the fulfilled reducer to set the user state.
[setUserAsync.fulfilled]: (state, action) => {
state.user = action.payload;
state.isLoggedIn = !!action.payload;
}
but in my dev tools I am seeing the following which is plain text of the password I input, in this case it is wrong, but when it's right it shows it just the same.
I don't think you need to be concerned. The production bundle of your app won't have the redux devtools enabled so the password can't linger there. And if you're using proper TLS (see https://security.stackexchange.com/questions/110415/is-it-ok-to-send-plain-text-password-over-https ), the password remains encrypted.
I have been trying to verify email with firebase but for some reason the emailVerified field never becomes true. I am successfully receiving the email link, when I open the link it redirects me back to the verify email page, and on the page I am parsing the url and capturing the oob code, and then passing into the applyActionCode function. When I do this it keeps telling me my code is invalid. Please help..
Signup function where I am sending the email verification (this part works)
const handleSignup = async (email, password) => {
try {
setLoading(true);
await signup(email, password).then(() => {
sendVerificationEmail(email);
});
// history.push('/home');
} catch (err) {
// firebase error
handleInputValidation(err);
setLoading(false);
}
};
VerifyEmail.jsx (this is the page I am redirected to when I open the link)
const EmailVerification = () => {
const {
currentUser,
isCorrectEmailLink,
signInWithEmail,
sendVerificationEmail,
verifyWithCode,
updateUserForVerification,
} = useAuth();
const currentUserEmail = currentUser.email;
const query = window?.location?.href?.split('oobCode=')[1];
const oob = query?.split('&mode')[0];
console.log(oob, 'here');
const verifyEmail = async (emailLink, email) => {
const isCorrect = isCorrectEmailLink(emailLink);
if (oob !== undefined) {
verifyWithCode(oob);
}
await updateUserForVerification();
};
useEffect(() => {
if (!currentUser.isVerified) {
verifyEmail(window.location.href);
}
}, []);
return (
<GridContainer>
<LoginContainer>
<WelcomeToDavie />
<AppletContainer>
<WelcomeText>Verify Your email adress</WelcomeText>
<Applet>
<FormName>Verify</FormName>
<LoginTextFieldContainer>
Verification code sent
<button onClick={() => sendVerificationEmail(currentUserEmail)} type="button">
send email
</button>
</LoginTextFieldContainer>
</Applet>
</AppletContainer>
</LoginContainer>
</GridContainer>
);
};
export default EmailVerification;
Here is the error I get when sending the oob code taken from the email link url
Please help me figure this out, been on this issue for a couple weeks now.
I am using Firebase's Web SDK in my Expo project, Redux is included too.
firebase.auth().signInWithEmailAndPassword() returns a Promise; I used .then() to handle the Promise and .catch() for any errors. This action is done in a separate file, not in the Login Screen because I dispatch an action in the then() clause.
Question is how would I be able to catch the error at the client's side? I can't seem to find any scenario that is similar to mine.
It's a school project :') Appreciate any help please!
LoginScreen:
I have tried calling the Toast from a useEffect and in the catch (error) { ... } clause too but to no avail. I know this way would not work but I cant seem to find any solution online.
const [loading, setLoading] = React.useState(false);
const [showAlert, setShowAlert] = React.useState(false);
const [alertMessage, setAlertMessage] = React.useState('');
const [alertStatus, setAlertStatus] = React.useState('');
const [error, setError] = React.useState(null);
const dispatch = useDispatch();
React.useEffect(() => {
if (error) {
// setAlertMessage(error);
// setShowAlert(true);
// setAlertStatus('danger');
Alert.alert('Error Occured', error, [{ text: 'Close' }]);
}
}, [error]);
const logInHandler = async () => {
try {
if (email.length === 0 || password.length === 0) {
setAlertMessage('You have empty fields!');
setShowAlert(true);
setAlertStatus('warning');
return;
}
dispatch(authActions.logIn(email, password));
setError(null);
setLoading(true);
setAlertMessage('Logging In...');
setShowAlert(true);
setAlertStatus('info');
} catch (err) {
// setAlertMessage(err.message);
// setShowAlert(true);
// setAlertStatus('danger');
setError(err.message);
setLoading(false);
}
};
return ( // i still don't get how to format properly when i paste codes here, sorry
{showAlert && (
<Toast // this is a custom toast in a separate component that i made that only shows when there is an error
message={alertMessage}
status={alertStatus}
hide={show => setShowAlert(show)}
/>
)}
);
File containing the firebase auth methods:
Throwing the error causes an Exception, but what i'm trying to achieve is to get the error message and display it using the custom Toast if there is an error from firebase
export const signUp = (email, password, name, gender) => dispatch => {
firebase
.auth()
.createUserWithEmailAndPassword(email.trim().toLowerCase(), password)
.then(res => {
firebase
.auth()
.currentUser.updateProfile({ displayName: name })
.then(() => {
dispatch({
type: STORE_USER_DATA,
id: res.user.uid,
user: res.user.displayName
});
dispatch({
type: SET_REGISTER,
isRegistering: true,
gender: gender
});
})
.catch(err => console.log('err.message', err.message));
})
.catch(err => {
let message = 'actions.signUp: An error has occured!';
let hasError =
err.code === 'auth/email-already-in-use' ||
err.code === 'auth/invalid-email' ||
err.code === 'auth/weak-password';
if (hasError) {
message = 'Invalid Credentials!';
}
throw new Error(message);
});
};
you probably need to dispatch the error instead of throwing the error - because the main usage of redux is state management so rather than throwing the error it makes more sense that you have a state that indicates if the login has failed or succeeded.
However i think it might be worth it to look at react-redux-firebase package its a clean integration between firebase and react/redux application - you could checkout authReducers, and authErrorReducers i think they might be helpful
I'll appreciate assistance with how to reauthenticate a user in Firebase. I wonder if it makes any sense adding all these great features if the documentation doesn't explain how to use it:
Currently, this is what I'm trying, and it ain't working. Errors as cannot read property 'credential' of undefined
In constructor:
constructor(#Inject(FirebaseApp) firebaseApp: any) {
this.auth = firebaseApp.auth();
console.log(this.auth);
}
then the function
changePassword(passwordData) {
if(passwordData.valid) {
console.log(passwordData.value);
// let us reauthenticate first irrespective of how long
// user's been logged in!
const user = this.auth.currentUser;
const credential = this.auth.EmailAuthProvider.credential(user.email, passwordData.value.oldpassword);
console.log(credential);
this.auth.reauthenticate(credential)
.then((_) => {
console.log('User reauthenticated');
this.auth.updatePassword(passwordData.value.newpassword)
.then((_) => {
console.log('Password changed');
})
.catch((error) => {
console.log(error);
})
})
.catch((error) => {
console.log(error);
})
}
}
The reauthenticate() method is called on a firebase.User, not on firebase.auth.Auth itself.
var user = firebase.app.auth().currentUser;
var credentials = firebase.auth.EmailAuthProvider.credential('puf#firebaseui.com', 'firebase');
user.reauthenticate(credentials);
Update (July 2017):
There are some breaking change in the 4.0 version of the Firebase Web SDK. From the release notes:
BREAKING: firebase.User.prototype.reauthenticate has been removed in favor of firebase.User.prototype.reauthenticateWithCredential.
As far as I can tell the reauthenticateWithCredentialis a drop-in replacement for the old method.
Here's some code that enabled users to (a) reauthenticate in Firebase and (b) change their passwords after reauthenticating for me. I researched for about an hour while writing this, so hopefully it saves someone a minute.
Wrote in VueJS:
changePassword() {
let self = this; // i use "self" to get around scope issues
var user = firebase.auth().currentUser;
var credential = firebase.auth.EmailAuthProvider.credential(
this.$store.state.userId, // references the user's email address
this.oldPassword
);
user.reauthenticateWithCredential(credential)
.then(function() {
// User re-authenticated.
user.updatePassword(self.newPassword)
.then(function() {
console.log("Password update successful!");
})
.catch(function(error) {
console.log(
"An error occurred while changing the password:",
error
);
});
})
.catch(function(error) {
console.log("Some kinda bug: ", error);
// An error happened.
});
Slight changes as of May 2019, see more details here. Code is as follows:
var user = firebase.auth().currentUser;
var credential = firebase.auth.EmailAuthProvider.credential(user.email, password);
// Prompt the user to re-provide their sign-in credentials
return user.reauthenticateWithCredential(credential);
Call changeEmail("new email","password") in onPressed directly to update the user email with no reauthentication required error
RaisedButton(
onPressed: () {
changeEmail(_emailController.text, _passwordController.text);
}
Future<void> changeEmail(String email, String password) async {
User user = await FirebaseAuth.instance.currentUser;
print(email);
print(password);
try {
try {
var authResult = await user.reauthenticateWithCredential(
EmailAuthProvider.getCredential(
email: user.email,
password: password,
),
);
user.updateEmail(email).then((_) {
print("Succesfull changed email");
_backthrow();
}).catchError((error) {
showAlertDialog(context, error.message);
print("email can't be changed" + error.toString());
});
return null;
} catch (e) {
print("2");
}
} catch (e) {
print(e.message);
showAlertDialog(context, e.message);
}
}
Hers a full example how to reauthenticate with Firebase
var pass = "abcdefg";
var user = firebase.auth().currentUser;
var credential = firebase.auth.EmailAuthProvider.credential(user.email, pass);
user.reauthenticateWithCredential(credential).then(() => {
console.log("Its good!");
}).catch((error) => {
console.log(error);
});
Since 2021: If you use Firebase JS API 9.x (the tree shakable version) this is the most recent way:
https://cloud.google.com/identity-platform/docs/web/reauth
With credentials
import { getAuth, reauthenticateWithCredential } from "firebase/auth";
const auth = getAuth();
const user = auth.currentUser;
// todo for you: prompt the user to re-provide their sign-in credentials
const credential = promptForCredentials();
reauthenticateWithCredential(user, credential).then(() => {
// ...
}).catch((error) => {
// ...
});
With popup
import { getAuth, reauthenticateWithPopup, OAuthProvider } from "firebase/auth";
const auth = getAuth();
// todo for you: change to appropriate provider
const provider = new OAuthProvider('apple.com');
reauthenticateWithPopup(auth.currentUser, provider)
.then((result) => {
// ...
})
.catch((error) => {
// ...
});
This is how I re-authenticate a user in Firebase:
import { getAuth, EmailAuthProvider, reauthenticateWithCredential } from "firebase/auth";
const auth = getAuth()
const reauthenticateUser = async (email, password) => {
const user = auth.currentUser;
try {
const credential = EmailAuthProvider.credential(email, password);
await reauthenticateWithCredential(user, credential)
} catch (error) {
Alert.alert("Error", "The email or password is incorrect. Please try again.")
}
}
I was getting that re-authentication error auth/requires-recent-login when saving the primary email.
I couldn't figure out how to implement that poorly documented reauthenticateWithCredential(credential) method, so, I simply logged-out the user and redirected to login page. It's a hack but It works like charm!
firebase.auth().signOut();