The ability to identify users is vital for maintaining the security of any applications. Equally important is the code that’s written to manage user identities, particularly when it comes to avoiding loopholes for unauthorized access to data held by an application. Writing authentication code without a framework or libraries available can take a ton of time to do right — not to mention the ongoing maintainance of that custom code.
This is where Firebase comes to the rescue. Its ready-to-use and intuitive methods make setting up effective user identity management on a site happen in no time. This tutorial will work us through on how to do that: implementing user registration, verification, and authentication.
Firebase v9 SDK introduces a new modular API surface, resulting in a change to several of its services, one of which is Firebase Authentication. This tutorial is current to the changes in v9.
To follow along with this tutorial, you should be familiar with React, React hooks, and Firebase version 8. You should also have a Google account and Node installed on your machine.
Table of Contents
- Setting up Firebase
- Cloning and setting up the starter repo
- Integrating Firebase into our React app
- Creating User Registration functionality
- Managing User State with React Context API
- Send a verification email to a registered user
- Working on the user profile page
- Creating a Private Route for the Profile component
- Creating login functionality
- Conclusion
- References
Setting up Firebase
Before we start using Firebase for our registration and authentication requirements, we have to first set up our Firebase project and also the authentication method we’re using.
To add a project, make sure you are logged into your Google account, then navigate to the Firebase console and click on Add project. From there, give the project a name (I’m using “Firebase-user-reg-auth”) and we should be all set to continue.
You may be prompted to enable Google Analytics at some point. There’s no need for it for this tutorial, so feel free to skip that step.
Firebase has various authentication methods for both mobile and web, but before we start using any of them, we have to first enable it on the Firebase Authentication page. From the sidebar menu, click on the Authentication icon, then, on the next page, click on Get started.
We are going to use Email/Password authentication. Click on it and we will be prompted with a screen to enable it, which is exactly what we want to do.
Cloning and setting up the starter repo
I have already created a simple template we can use for this tutorial so that we can focus specifically on learning how to implement the functionalities. So what we need to do now is clone the GitHub repo.
Fire up your terminal. Here’s what we can run from the command line:
git clone -b starter https://github.com/Tammibriggs/Firebase_user_auth.git cd Firebase_user_auth npm install
I have also included Firebase version 9 in the dependency object of the package.json
file. So, by running the npm install
command, Firebase v9 — along with all other dependencies — will be installed.
With done that, let’s start the app with npm start
!
Integrating Firebase into our React app
To integrate Firebase, we need to first get the web configuration object and then use it to initialize Firebase in our React app. Go over to the Firebase project page and we will see a set of options as icons like this:
Click on the web (</>
) icon to configure our Firebase project for the web, and we will see a page like this:
Enter firebase-user-auth as the name of the web app. After that, click on the Register app button, which takes us to the next step where our firebaseConfig
object is provided.
Copy the config to the clipboard as we will need it later on to initialize Firebase. Then click on the Continue to console button to complete the process.
Now, let’s initialize Firebase and Firebase Authentication so that we can start using them in our app. In the src
directory of our React app, create a firebase.js
file and add the following imports:
// src/firebase.js
import { initializeApp } from 'firebase/app'
import {getAuth} from 'firebase/auth'
Now, paste the config we copied earlier after the imports and add the following lines of code to initialize Firebase and Firebase Authentication.
// src/firebase.js
const app = initializeApp(firebaseConfig)
const auth = getAuth(app) export {auth}
Our firebase.js
file should now look something like this:
// src.firebase.js
import { initializeApp } from "firebase/app"
import { getAuth } from "firebase/auth" const firebaseConfig = { apiKey: "API_KEY", authDomain: "AUTH_DOMAIN", projectId: "PROJECT_ID", storageBucket: "STORAGE_BUCKET", messagingSenderId: "MESSAGING_SENDER_ID", appId: "APP_ID"
} // Initialize Firebase and Firebase Authentication
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
export {auth}
Next up, we’re going to cover how to use the ready-to-use functions provided by Firebase to add registration, email verification, and login functionality to the template we cloned.
Creating User Registration functionality
In Firebase version 9, we can build functionality for user registration with the createUserWithEmailAndPassword
function. This function takes three arguments:
- auth instance/service
- password
Services are always passed as the first arguments in version 9. In our case, it’s the auth service.
To create this functionality, we will be working with the Register.js
file in the src
directory of our cloned template. What I did in this file is create three form fields — email, password, and confirm password — and input is controlled by the state. Now, let’s get to business.
Let’s start by adding a function that validates the password and confirm password inputs, checking if they are not empty and are the same: Add the following lines of code after the states in the Register
component:
// src/Register.js
// ... const validatePassword = () => { let isValid = true if (password !== '' && confirmPassword !== ''){ if (password !== confirmPassword) { isValid = false setError('Passwords does not match') } } return isValid
} // ...
In the above function, we return an isValid
variable which can return either true or false based on the validity of the passwords. Later on, we will use the value of this variable to create a condition where the Firebase function responsible for registering users will only be invoked if isValid
is true.
To create the registration functionality, let’s start by making the necessary imports to the Register.js
file:
// src/Register.js
import {auth} from './firebase'
import {createUserWithEmailAndPassword} from 'firebase/auth'
Now, add the following lines of code after the validatePassword
password function:
// src/Register.js
// ... const register = e => { e.preventDefault() setError('') if(validatePassword()) { // Create a new user with email and password using firebase createUserWithEmailAndPassword(auth, email, password) .then((res) => { console.log(res.user) }) .catch(err => setError(err.message)) } setEmail('') setPassword('') setConfirmPassword('')
} // ...
In the above function, we set a condition to call the createUserWithEmailAndPassword
function only when the value returning from validatePassword
is true.
For this to start working, let’s call the register
function when the form is submitted. We can do this by adding an onSubmit
event to the form. Modify the opening tag of the registration_form
to look like this:
// src/Register.js
<form onSubmit={register} name='registration_form'>
With this, we can now register a new user on our site. To test this by going over to http://localhost:3000/register
in the browser, filling in the form, then clicking on the Register button.
After clicking the Register button, if we open the browser’s console we will see details of the newly registered user.
Managing User State with React Context API
Context API is a way to share data with components at any level of the React component tree without having to pass it down as props. Since a user might be required by a different component in the tree, using the Context API is great for managing the user state.
Before we start using the Context API, there are a few things we need to set up:
- Create a context object using the
createContext()
method - Pass the components we want to share the user state with as children of Context.Provider
- Pass the value we want the children/consuming component to access as props to
Context.Provider
Let’s get to it. In the src
directory, create an AuthContext.js
file and add the following lines of code to it:
// src/AuthContext.js
import React, {useContext} from 'react' const AuthContext = React.createContext() export function AuthProvider({children, value}) { return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> )
} export function useAuthValue(){ return useContext(AuthContext)
}
In the above code, we created a context called AuthContext
along with that we also created two other functions that will allow us to easily use the Context API which is AuthProvider
and useAuthValue
.
The AuthProvider
function allows us to share the value of the user’s state to all the children of AuthContext.Provider
while useAuthValue
allows us to easily access the value passed to AuthContext.Provider
.
Now, to provide the children and value props to AuthProvider
, modify the App.js
file to look something like this:
// src/App.js
// ...
import {useState} from 'react'
import {AuthProvider} from './AuthContext' function App() { const [currentUser, setCurrentUser] = useState(null) return ( <Router> <AuthProvider value={{currentUser}}> <Switch> ... </Switch> </AuthProvider> </Router> );
} export default App;
Here, we’re wrapping AuthProvider
around the components rendered by App
. This way, the currentUser
value supplied to AuthProvider
will be available for use by all the components in our app except the App
component.
That’s it as far as setting up the Context API! To use it, we have to import the useAuthValue
function and invoke it in any of the child components of AuthProvider
, like Login
. The code looks something like this:
import { useAuthValue } from "./AuthContext" function childOfAuthProvider(){ const {currentUser} = useAuthValue() console.log(currentUser) return ...
}
Right now, currentUser
will always be null
because we are not setting its value to anything. To set its value, we need to first get the current user from Firebase which can be done either by using the auth instance that was initialized in our firebase.js
file (auth.currentUser
), or the onAuthStateChanged
function, which actually happens to be the recommended way to get the current user. That way, we ensure that the Auth object isn’t in an intermediate state — such as initialization — when we get the current user.
In the App.js
file, add a useEffect
import along with useState
and also add the following imports:
// src/App.js
import {useState, useEffect} from 'react'
import {auth} from './firebase'
import {onAuthStateChanged} from 'firebase/auth'
Now add the following line of code after the currentUser
state in the App component:
// src/App.js
// ... useEffect(() => { onAuthStateChanged(auth, (user) => { setCurrentUser(user) })
}, []) // ...
In the above code, we are getting the current user and setting it in the state when the component renders. Now when we register a user the currentUser
state will be set with an object containing the user’s info.
Send a verification email to a registered user
Once a user is registered, we want them to verify their email address before being able to access the homepage of our site. We can use the sendEmailVerification
function for this. It takes only one argument which is the object of the currently registered user. When invoked, Firebase sends an email to the registered user’s email address with a link where the user can verify their email.
Let’s head over to the Register.js
file and modify the Link
and createUserWithEmailAndPassword
import to look like this:
// src/Register.js
import {useHistory, Link} from 'react-router-dom'
import {createUserWithEmailAndPassword, sendEmailVerification} from 'firebase/auth'
In the above code, we have also imported the useHistory
hook. This will help us access and manipulate the browser’s history which, in short, means we can use it to switch between pages in our app. But before we can use it we need to call it, so let’s add the following line of code after the error
state:
// src/Register.js
// ...
const history = useHistory() // ...
Now, modify the .then
method of the createUserWithEmailAndPassword
function to look like this:
// src/Register.js
// ...
.then(() => { sendEmailVerification(auth.currentUser) .then(() => { history.push('/verify-email') }).catch((err) => alert(err.message))
})
// ...
What’s happening here is that when a user registers a valid email address, they will be sent a verification email, then taken to the verify-email page.
There are several things we need to do on this page:
- Display the user’s email after the part that says “A verification email has been sent to:”
- Make the Resend Email button work
- Create functionality for disabling the Resend Email button for 60 seconds after it is clicked
- Take the user to their profile page once the email has been verified
We will start by displaying the registered user’s email. This calls for the use of the AuthContext
we created earlier. In the VerifyEmail.js
file, add the following import:
// src/VerifyEmail.js
import {useAuthValue} from './AuthContext'
Then, add the following code before the return
statement in the VerifyEmail
component:
// src/VerifyEmail.js
const {currentUser} = useAuthValue()
Now, to display the email, add the following code after the <br/>
tag in the return
statement.
// src/VerifyEmail.js
// ...
<span>{currentUser?.email}</span>
// ...
In the above code, we are using optional chaining to get the user’s email so that when the email is null our code will throw no errors.
Now, when we refresh the verify-email page, we should see the email of the registered user.
Let’s move to the next thing which is making the Resend Email button work. First, let’s make the necessary imports. Add the following imports to the VerifyEmail.js
file:
// src/VerifyEmail.js
import {useState} from 'react'
import {auth} from './firebase'
import {sendEmailVerification} from 'firebase/auth'
Now, let’s add a state that will be responsible for disabling and enabling the Resend Email button based on whether or not the verification email has been sent. This code goes after currentUser
in the VerifyEmail
component:
// src/VerifyEmail.js
const [buttonDisabled, setButtonDisabled] = useState(false)
For the function that handles resending the verification email and disabling/enabling the button, we need this after the buttonDisabled
state:
// src/VerifyEmail.js
// ... const resendEmailVerification = () => { setButtonDisabled(true) sendEmailVerification(auth.currentUser) .then(() => { setButtonDisabled(false) }).catch((err) => { alert(err.message) setButtonDisabled(false) })
} // ...
Next, in the return
statement, modify the Resend Email button like this:
// ...
<button onClick={resendEmailVerification} disabled={buttonDisabled} >Resend Email</button>
// ...
Now, if we go over to the verify-email page and click the button, another email will be sent to us. But there is a problem with how we created this functionality because if we try to click the button again in less than a minute, we get an error from Firebase saying we sent too many requests. This is because Firebase has a one minute interval before being able to send another email to the same address. That’s the net thing we need to address.
What we need to do is make the button stay disabled for 60 seconds (or more) after a verification email is sent. We can enhance the user experience a bit by displaying a countdown timer in Resend Email button to let the user know the button is only temporarily disabled.
In the VerifyEmail.js
file, add a useEffect
import:
import {useState, useEffect} from 'react'
Next, add the following after the buttonDisabled
state:
// src/VerifyEmail.js
const [time, setTime] = useState(60)
const [timeActive, setTimeActive] = useState(false)
In the above code, we have created a time
state which will be used for the 60-second countdown and also a timeActive
state which will be used to control when the count down will start.
Add the following lines of code after the states we just created:
// src/VerifyEmail.js
// ... useEffect(() => { let interval = null if(timeActive && time !== 0 ){ interval = setInterval(() => { setTime((time) => time - 1) }, 1000) }else if(time === 0){ setTimeActive(false) setTime(60) clearInterval(interval) } return () => clearInterval(interval);
}, [timeActive, time]) // ...
In the above code, we created a useEffect
hook that only runs when the timeActive
or time
state changes. In this hook, we are decreasing the previous value of the time
state by one every second using the setInterval
method, then we are stopping the decrementing of the time
state when its value equals zero.
Since the useEffect
hook is dependent on the timeActive
and time
state, one of these states has to change before the time count down can start. Changing the time
state is not an option because the countdown has to start only when a verification email has been sent. So, instead, we need to change the timeActive
state.
In the resendEmailVerification
function, modify the .then
method of sendEmailVerification
to look like this:
// src/VerifyEmail.js
// ...
.then(() => { setButtonDisabled(false) setTimeActive(true)
})
// ...
Now, when an email is sent, the timeActive
state will change to true and the count down will start. In the code above we need to change how we are disabling the button because, when the count down is active, we want the disabled button.
We will do that shortly, but right now, let’s make the countdown timer visible to the user. Modify the Resend Email button to look like this:
// src/VerifyEmail.js
<button onClick={resendEmailVerification} disabled={buttonDisabled}
>Resend Email {timeActive && time}</button>
To keep the button in a disabled state while the countdown is active, let’s modify the disabled
attribute of the button to look like this:
disabled={timeActive}
With this, the button will be disabled for a minute when a verification email is sent. Now we can go ahead and remove the buttonDisabled
state from our code.
Although this functionality works, there is still one problem with how we implemented it: when a user registers and is taken to the verify-email page when they have not received an email yet, they may try to click the Resend Email button, and if they do that in less than a minute, Firebase will error out again because we’ve made too many requests.
To fix this, we need to make the Resend Email button disabled for 60 seconds after an email is sent to the newly registered user. This means we need a way to change the timeActive
state within the Register
component. We can also use the Context API for this. It will allow us to globally manipulate and access the timeActive
state.
Let’s make a few modifications to our code to make things work properly. In the VerifyEmail
component, cut the timeActive
state and paste it into the App
component after the currentUser
state.
// src/App.js
function App() { // ... const [timeActive, setTimeActive] = useState(false) // ...
Next, put timeActive
and setTimeActive
inside the object of AuthProvider
value prop. It should look like this:
// src/App.js
// ...
<AuthProvider value={{currentUser, timeActive, setTimeActive}}>
// ...
Now we can access timeActive
and setTimeActive
within the children of AuthProvider
. To fix the error in our code, go to the VerifyEmail.js
file and de-structure both timeActive
and setTimeActive
from useAuthProvider
:
// src/VerifyEmail.js
const {timeActive, setTimeActive} = useAuthValue()
Now, to change the timeActive
state after a verification email has been sent to the registered user, add the following import in the Register.js
file:
// src/Register.js
import {useAuthValue} from './AuthContext'
Next, de-structure setTimeActive
from useAuthValue
with this snippet among the other states in the Register
component:
// src/Register.js
const {setTimeActive} = useAuthValue()
Finally, in the register
function, set the timeActive
state with the .then
the method of sendEmailVerification
:
// src/Register.js
// ...
.then(() => { setTimeActive(true) history.push('/verify-email')
})
// ...
With this, a user will be able to send a verification email without getting any errors from Firebase.
The last thing to fix concerning user verification is to take the user to their profile page after they have verified their email. To do this, we will use a reload
function in the currentUser
object. It allows us to reload the user object coming from Firebase, that way we will know when something has changed.
First, let’s make the needed imports. In the VerifyEmail.js
file, let’s add this:
// src/VerifyEmail.js
import {useHistory} from 'react-router-dom'
We are importing useHistory
so that we can use to navigate the user to the profile page. Next, add the following line of code after the states:
// src/VerifyEmail.js
const history = useHistory()
And, finally, add the following lines of code after the history
variable:
// src/VerifyEmail.js
// ... useEffect(() => { const interval = setInterval(() => { currentUser?.reload() .then(() => { if(currentUser?.emailVerified){ clearInterval(interval) history.push('/') } }) .catch((err) => { alert(err.message) }) }, 1000)
}, [history, currentUser]) // ...
In the above code, we are running the reload
function every one second until the user’s email has been verified, and, if it has, we are navigating the user to their profile page.
To test this, let’s verify our email by following the instructions in the email sent from Firebase. If all is good, we will be automatically taken to our profile page.
Right now the profile page is showing no user data and he Sign Out link does not work. That’s ur next task.
Working on the user profile page
Let’s start by displaying the Email and Email verified values. For this, we will make use of the currentUser
state in AuthContext
. What we need to do is import useAuthValue
, de-structure currentUser
from it, and then display the Email and Email verified value from the user object.
Here is what the Profile.js
file should look like:
// src/Profile.js
import './profile.css'
import {useAuthValue} from './AuthContext' function Profile() { const {currentUser} = useAuthValue() return ( <div className='center'> <div className='profile'> <h1>Profile</h1> <p><strong>Email: </strong>{currentUser?.email}</p> <p> <strong>Email verified: </strong> {`${currentUser?.emailVerified}`} </p> <span>Sign Out</span> </div> </div> )
} export default Profile
With this, the Email and Email verified value should now be displayed on our profile page.
To get the sign out functionality working, we will use the signOut
function. It takes only one argument, which is the auth
instance. So, in Profile.js
. let’s add those imports.
// src/Profile.js
import { signOut } from 'firebase/auth' import { auth } from './firebase'
Now, in the return
statement, modify the <span>
that contains “Sign Out” so that is calls the signOut
function when clicked:
// src/Profile.js
// ...
<span onClick={() => signOut(auth)}>Sign Out</span>
// ...
Creating a Private Route for the Profile component
Right now, even with an unverified email address, a user can access the profile page. We don’t want that. Unverified users should be redirected to the login page when they try to access the profile. This is where private routes come in.
In the src
directory, let’s create a new PrivateRoute.js
file and add the following code to it:
// src/PrivateRoute.js
import {Route, Redirect} from 'react-router-dom'
import {useAuthValue} from './AuthContext' export default function PrivateRoute({component:Component, ...rest}) { const {currentUser} = useAuthValue() return ( <Route {...rest} render={props => { return currentUser?.emailVerified ? <Component {...props} /> : <Redirect to='/login' /> }}> </Route> )
}
This PrivateRoute
is almost similar to using the Route
. The difference is that we are using a render
prop to redirect the user to the profile page if their email is unverified.
We want the profile page to be private, so well import PrivateRoute
:
// src/App.js
import PrivateRoute from './PrivateRoute'
Then we can replace Route
with PrivateRoute
in the Profile
component. The Profile
route should now look like this:
// src/App.js
<PrivateRoute exact path="/" component={Profile} />
Nice! We have made the profile page accessible only to users with verified emails.
Creating login functionality
Since only users with verified emails can access their profile page when logged in with the signInWithEmailAndPassword
function, we also need to check if their email has been verified and, if it is unverified, the user should be redirected to the verify-email page where the sixty-second countdown should also start.
These are the imports we need to add to the Login.js
file:
import {signInWithEmailAndPassword, sendEmailVerification} from 'firebase/auth'
import {auth} from './firebase'
import {useHistory} from 'react-router-dom'
import {useAuthValue} from './AuthContext'
Next, add the following line of code among the states in the Login
component.
// src/Login.js
const {setTimeActive} = useAuthValue()
const history = useHistory()
Then add the following function after the history
variable:
// src/Login.js
// ... const login = e => { e.preventDefault() signInWithEmailAndPassword(auth, email, password) .then(() => { if(!auth.currentUser.emailVerified) { sendEmailVerification(auth.currentUser) .then(() => { setTimeActive(true) history.push('/verify-email') }) .catch(err => alert(err.message)) }else{ history.push('/') } }) .catch(err => setError(err.message))
} // ...
This logs in a user and then check if whether they are verified or not. If they are verified, we navigate them to their profile page. But if they are unverified, we send a verification email, then redirect them to the verify-email page.
All we need to do to make this work is call the login
function when the form is submitted. So, let’s modify the opening tag of the login_form
to this:
// src/Login.js
<form onSubmit={login} name='login_form'>
And, hey, we’re done!
Conclusion
In this tutorial, we have learned how to use version 9 of the Firebase Authentication to build a fully functioning user registration and authentication service in React. Is it super easy? No, there are a few thing we need to juggle. But is it a heck of a lot easier than building our own service from scratch? You bet it is! And that’s what I hope you got from reading this.
References
- Get Started with Firebase Authentication on Websites (Firebase documentation)
- Manage Users in Firebase (Firebase documentation)
User Registration and Auth Using Firebase and React originally published on CSS-Tricks. You should get the newsletter and become a supporter.