Authentication is one of those things that just always seems to take a lot more effort than we want it to, yet it's always a feature every website needs.
Firebase makes this process super easy. So today lets create a React application where we use firebase authentication and router guards.
It will have a total of 3 pages. One for signing up, another for logging, and a home page that is only accessible if the user is authenticated.
You can find the full write up at codingwithjustin.com and source code on github.
Firebase
We'll need to set up a firebase project to get started.
Head over to Firebase and create a new application. The process should be straightforward and only take a few seconds. We'll also need to enable the auth options before we start building anything. First, make sure you enable email/password in the Authentication tab, by clicking on Sign-methods.
I'll also be using version 9 of firebase which is currently in beta. It makes the firebase tree shakeable as well as provides some other improvements.
Project Setup
We'll need to create a new project using the create react app CLI.
npx create-react-app firebase-auth-react
Once completed we'll also install react-router-dom
and firebase@beta
for version 9.
yarn add react-router-dom firebase@beta
Next I'll create a firebase helper file called firebase.js
.
import { getAuth, onAuthStateChanged } from '@firebase/auth'
import { initializeApp } from 'firebase/app'
import { useState, useEffect, useContext, createContext } from 'react'
export const firebaseApp = initializeApp({ /* config */ })
export const AuthContext = createContext()
export const AuthContextProvider = props => {
const [user, setUser] = useState()
const [error, setError] = useState()
useEffect(() => {
const unsubscribe = onAuthStateChanged(getAuth(), setUser, setError)
return () => unsubscribe()
}, [])
return <AuthContext.Provider value={{ user, error }} {...props} />
}
export const useAuthState = () => {
const auth = useContext(AuthContext)
return { ...auth, isAuthenticated: auth.user != null }
}
Here we'll initialize our configuration using the values we got from creating a project. We'll also create an auth context for holding the state of the current user signed in.
Context in react is a tool that allows you to share state throughout the whole react component without having to pass it down by props. Instead, we can initialize a Context Provider
, pass in our state as value, and then we can access it anywhere by calling useContext
with our context object. In our case will want to pass in the user's state which we get from the onAuthStateChanged
listener. We'll also want to make sure we unsubscribe from this event when the component is unmounted.
Routing
In our App.js
we'll need to add our routing option and link these to each of our pages. However, doing this won't protect our routes from unauthenticated users. To protect our routes we'll create a custom component which Ill call AuthenticatedRoute
.
const AuthenticatedRoute = ({ component: C, ...props }) => {
const { isAuthenticated } = useAuthState()
console.log(`AuthenticatedRoute: ${isAuthenticated}`)
return (
<Route
{...props}
render={routeProps =>
isAuthenticated ? <C {...routeProps} /> : <Redirect to="/login" />
}
/>
)
}
We'll call the useAuthState
hook we created earlier to check if the user is authenticated. If they are authenticated we'll render the page, otherwise, we'll redirect them to the login page.
Let's also create a simple UnauthenticatedRoute that will use for the login page. This component is similar to the logic above expect we will only want to render the component if the user is not authenticated.
const UnauthenticatedRoute = ({ component: C, ...props }) => {
const { isAuthenticated } = useAuthState()
console.log(`UnauthenticatedRoute: ${isAuthenticated}`)
return (
<Route
{...props}
render={routeProps =>
!isAuthenticated ? <C {...routeProps} /> : <Redirect to="/" />
}
/>
)
}
It's also worth mentioning, you might want to add a loading sign-on in your app while the auth check is being run. This way you don't flash a page every time you refresh.
Pages
Now, let's go through each page and those up.
Login
For the login page, we'll create a form that asks the user for an email address and password. When the user clicks the submit button, we'll grab those two values from the form element and pass them into the signInWithEmailAndPassword
function. Once it's successful the user will be considered logged in and will automatically be redirected to the home page.
import { useCallback } from 'react'
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
export const Login = () => {
const handleSubmit = useCallback(async e => {
e.preventDefault()
const { email, password } = e.target.elements
const auth = getAuth()
try {
await signInWithEmailAndPassword(auth, email.value, password.value)
} catch (e) {
alert(e.message)
}
}, [])
return (
<>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input name="email" placeholder="email" type="email" />
<input name="password" placeholder="password" type="password" />
<button type="submit">Login</button>
</form>
</>
)
}
I recommend you add better error handling here but I'm going to wrap this in a try-catch statement and alert the user with any error messages.
If we wanted to redirect to a specific URL we could call the useLocation
hook from the react router and push a path onto it.
Signup
The signup page is also going to be very similar, we'll create another form that asks for their email and password. On submit we'll grab those values and call the createUserWithEmailAndPassword
function. If the user signs in is successfully they will automatically get redirect to the home page.
import { useCallback } from 'react'
import { getAuth, createUserWithEmailAndPassword } from 'firebase/auth'
export const SignUp = () => {
const handleSubmit = useCallback(async e => {
e.preventDefault()
const { email, password } = e.target.elements
const auth = getAuth()
try {
await createUserWithEmailAndPassword(auth, email.value, password.value)
} catch (e) {
alert(e.message)
}
}, [])
return (
<>
<h1>Sign Up</h1>
<form onSubmit={handleSubmit}>
<input name="email" placeholder="email" type="email" />
<input name="password" placeholder="password" type="password" />
<button type="submit">Sign Up</button>
</form>
</>
)
}
Home Page
For the Home page, We'll put a nice welcome message and show the user's email. We'll also create a button that will call the auth signout function.
import { getAuth, signOut } from 'firebase/auth'
import { useAuthState } from './firebase'
export const Home = () => {
const { user } = useAuthState()
return (
<>
<h1>Welcome {user?.email}</h1>
<button onClick={() => signOut(getAuth())}>Sign out</button>
</>
)
}
Conclusion
Adding authentication and access control to your application doesn't have to be a hassle. Both the setup step and, more importantly, the maintenance over time, are handled with modern platforms like Firebase.
I have a community over on discord if you'd like to learn more. You should also check out my website codingwithjustin.com where I post more content similar to this one.
Top comments (2)
There is a problem with this method. It makes it impossible to reach the url.
so the flow is I visited /a endpoint. It will be redirected to the /login and post that it will be redirected to / path.
I added a success url in /login to be the /a and it is ending up in an infinite redirect now.
Guys, there is new competitor in the town Supabase. If someone wants to ditch the google policies and maintain our app on open source code, then definitely supabase is your go to. I'm not a promoter just don't like firebase, then heard about supabse. One could easily maintain auth and database on supabase while host and serve functions on netlify. Great combo!
Plus, thanks to the author of this post for writing this infomercial. Nice post!