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 initializationconst 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 loginconst 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 Parametersconst 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 passwordconst 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";// Componentsimport Login from "./Login";import Home from "./Home";import ForgotPasswordModal from "./ForgotPasswordModal";import ResetPasswordModal from "./ResetPasswordModal";// MongoDB Stitch initializationconst 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 Parametersconst 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 modalconst [forgotPasswordModalOpen, setForgotPasswordModalOpen] = useState(false);// State to control the Reset Password modalconst [resetPasswordModalOpen, setResetPasswordModalOpen] = useState(false);// State to store the user objectconst [user, setUser] = useState({});// Function to attempt a user loginconst 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 resetconst 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 messageconsole.log("Error sending password reset email:", err);});};// Function to confirm a new passwordconst 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 messageconsole.log("Error resetting password:", err);});};// On page load, check for URL parametersuseEffect(() => {if (token && tokenID) {setResetPasswordModalOpen(true);}}, []);return (<>{user ? (<Home user={user} />) : (<Loginlogin={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.