MongoDB Stitch Email/Password Authentication in React


May 2020

MongoDB Stitch has quickly become my favorite solution to build a Serverless app with authentication, cloud functions and seamless access to a MongoDB Atlas database.

The official docs are clear and well organized, but it took me some work to figure out certain kinks of the username/password authentication. So in this article I will share three key learnings:

  • using a ternary expression when defining the Client;
  • handling your User object to detect a successful login;
  • handling Password Reset through a simple modal.

1. Defining the Client

First of all, we want to import the MongoDB Stitch SDK at the top of our main file. That is typically App.js in React.

import {
Stitch,
RemoteMongoClient,
UserPasswordCredential,
UserPasswordAuthProviderClient,
} from "mongodb-stitch-browser-sdk";

Then we have a few constants to define. Of course I'm assuming that you have already configured your Atlas cluster and Stitch app on the MongoDB Web GUI, hence you know your APP ID as well as the names of your API and Database.

// MongoDB Stitch initialization
const APP_ID = "<your-app-id-from-stitch>";
const client = Stitch.hasAppClient(APP_ID)
? Stitch.getAppClient(APP_ID)
: Stitch.initializeDefaultAppClient(APP_ID);
const emailPassClient = client.auth.getProviderClient(
UserPasswordAuthProviderClient.factory
);
const mongodb = client.getServiceClient(
RemoteMongoClient.factory,
"<your-api-name-from-stitch>"
);
const db = mongodb.db("<your-database-name>");
const { user } = client.auth;

The ternary expression to define client is crucial. If we simply say const client = Stitch.initializeDefaultAppClient(APP_ID) as suggested by the official docs, everything will work well the first time, but on subsequent re-renders we will get an error saying that we are trying to initialize the same Stitch instance multiple times.

2. Handling the User Object

We have seen above that the User object is typically declared upfront, before writing the main App function.

Based on the existence of that object, we can then use a ternary expression in JSX to welcome authenticated users while sending visitors to the login page:

{
user ? <Home /> : <Login />;
}

If App.js initially loads the <Login /> component, we need to make sure that a subsequent successful login will move the user from <Login /> to <Home /> through a re-render. We do this by saving the user object in state: const [user, setUser] = useState({}), and then setUser(authedUser).

// Function to attempt a user login
const attemptLogin = () => {
const credential = new UserPasswordCredential(
newUser.email.toLowerCase(),
newUser.password
);
client.auth
.loginWithCredential(credential)
.then(authedUser => {
console.log(`Login successful: ${authedUser.id}`);
setUser(authedUser);
})
.catch(err => {
setUI({ ...UI, loginError: true });
console.log(`Login failed: ${err}`);
});
};

3. Password Reset

When a user requests a password reset, MongoDB Stitch sends out an email with a link (to a page of your choice) containing two parameters: token and tokenID. Of course you can create a dedicated landing page, but my preference is to send users back to the home page, where we will open a modal if token and tokenID are present.

The first step is to try and capture those values:

// URL Parameters
const url = window.location.search;
const params = new URLSearchParams(url);
const token = params.get("token");
const tokenID = params.get("tokenId");

If those values are present in the URL, we open a modal on page load:

useEffect(() => {
if (token && tokenID) {
setResetPasswordModalOpen(true);
}
}, []);

Finally, a simple form inside that modal will trigger the setNewPassword function:

// Function to confirm a new password
const setNewPassword = () => {
emailPassClient
.resetPassword(token, tokenID, newUser.password)
.then(() => {
setResetPasswordModalOpen(false);
setUI({ ...UI, passwordChanged: true });
})
.catch(err => {
setResetPasswordModalOpen(false);
setUI({ ...UI, passwordChangeError: true });
console.log("Error resetting password:", err);
});
};

In the code above, newUser is an object kept in state and populated by the user as they type. And setUI is part of another useState statement with the sole purpose of controlling courtesy messages in the JSX.

Conclusion

As promised, I only focused on three key areas that caused me pain during my MongoDB Stitch learning curve. The rest of the code should be pretty straightforward in the official docs. If not, here's my complete App.js file:

import React, { useState, useEffect } from "react";
import {
Stitch,
RemoteMongoClient,
UserPasswordCredential,
UserPasswordAuthProviderClient,
} from "mongodb-stitch-browser-sdk";
// Components
import Login from "./Login";
import Home from "./Home";
import ForgotPasswordModal from "./ForgotPasswordModal";
import ResetPasswordModal from "./ResetPasswordModal";
// MongoDB Stitch initialization
const APP_ID = "<your-app-id-from-stitch>";
const client = Stitch.hasAppClient(APP_ID)
? Stitch.getAppClient(APP_ID)
: Stitch.initializeDefaultAppClient(APP_ID);
const emailPassClient = client.auth.getProviderClient(
UserPasswordAuthProviderClient.factory
);
const mongodb = client.getServiceClient(
RemoteMongoClient.factory,
"<your-api-name-from-stitch>"
);
const db = mongodb.db("<your-database-name>");
// URL Parameters
const url = window.location.search;
const params = new URLSearchParams(url);
const token = params.get("token");
const tokenID = params.get("tokenId");
const App = () => {
// State to control the Forgotten Password modal
const [forgotPasswordModalOpen, setForgotPasswordModalOpen] = useState(false);
// State to control the Reset Password modal
const [resetPasswordModalOpen, setResetPasswordModalOpen] = useState(false);
// State to store the user object
const [user, setUser] = useState({});
// Function to attempt a user login
const attemptLogin = newUser => {
const credential = new UserPasswordCredential(
newUser.email.toLowerCase(),
newUser.password
);
client.auth
.loginWithCredential(credential)
.then(authedUser => {
console.log(`Login successful: ${authedUser.id}`);
setUser(authedUser);
})
.catch(err => {
console.log(`Login failed: ${err}`);
// Add code to show an error message
});
};
// Function to initiate a password reset
const initiatePasswordReset = newUser => {
emailPassClient
.sendResetPasswordEmail(newUser.email.toLowerCase())
.then(() => {
setForgotPasswordModalOpen(false);
// Add code to show a confirmation message
})
.catch(err => {
setForgotPasswordModalOpen(false);
// Add code to show an error message
console.log("Error sending password reset email:", err);
});
};
// Function to confirm a new password
const setNewPassword = newUser => {
emailPassClient
.resetPassword(token, tokenID, newUser.password)
.then(() => {
setResetPasswordModalOpen(false);
// Add code to show a confirmation message
})
.catch(err => {
setResetPasswordModalOpen(false);
// Add code to show an error message
console.log("Error resetting password:", err);
});
};
// On page load, check for URL parameters
useEffect(() => {
if (token && tokenID) {
setResetPasswordModalOpen(true);
}
}, []);
return (
<>
{user ? (
<Home user={user} />
) : (
<Login
login={attemptLogin}
resetPwd={initiatePasswordReset}
setPwd={setNewPassword}
/>
)}
{forgotPasswordModalOpen && (
<ForgotPasswordModal setModal={setForgotPasswordModalOpen} />
)}
{resetPasswordModalOpen && (
<ResetPasswordModal setModal={setResetPasswordModalOpen} />
)}
</>
);
};
export default App;

I hope this helps! If you have any questions or comments, feel free to reach me on Twitter.

© 2024 Stefano Picker