Coding With Justin

VideosCoursesPricing

Firebase Authentication Tutorial with Private Routes in React and Vue

Today we'll create both a React and Vue application where we use firebase authentication with router guards to allow users to sign in with a custom email address. 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.

firebasereactvueauthentication

Justin Brooks

May 23, 2021

Firebase Authentication Tutorial with Private Routes in React and Vue

FirebaseReactReact Project SetupReact RoutingReact PagesVueVue Project SetupVue RoutingVue PagesConclusion

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 we'll create both a React and Vue application where we use firebase authentication and router guards and allow users to sign in with a custom email address.

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.

Firebase

In both cases for creating the app, we'll need to set up a firebase project. 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.

React

Let's get started with the most popular option, React.

React 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 need to also install react-router-dom and firebase@beta for version 9.

yarn add react-router-dom firebase@beta

To start we'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.

React 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.

React 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>
</>
)
}

Vue

This takes care of our react integration now let's move on to my personal favorite, Vue.

Vue Project Setup

Let's get started by creating a Vue project using the CLI tool. We'll be using the Vue 3 composition API extensively in this tutorial and we'll also want to make sure we enable the Vue router so we can create different pages.

vue create firebase-auth-vue

Once the Vue project is set up we will also need to install firebase beta to get access to Firestore version 9 API.

yarn add firebase@beta

Next, I'll create a firebase.js file to set up and initialize our firebase application.

import { initializeApp } from 'firebase/app'
export const firebaseApp = initializeApp({ /* config */})

This file will actually contain all we need for interacting with firebase. So let's also create some functions we use in our Vue application.

The first one we will create is for handling authentication. This will listen to auth state changes and assign the value to a user reactive variable. We also need to make sure we unsubscribe from this even when the component is unmounted. We can add a computed property here to check if a user is logged in or not since firebase will return null if a user is not authenticated. This will be useful when we start creating components.

import { getAuth, onAuthStateChanged } from 'firebase/auth'
import { ref, computed, onMounted, onUnmounted } from 'vue'
export const useAuthState = () => {
const user = ref(null)
const error = ref(null)
const auth = getAuth()
let unsubscribe
onMounted(() => {
unsubscribe = onAuthStateChanged(
auth,
u => (user.value = u),
e => (error.value = e)
)
})
onUnmounted(() => unsubscribe())
const isAuthenticated = computed(() => user.value != null)
return { user, error, isAuthenticated }
}

We'll also need a way to get the current user logged in through a promise. This will be useful when we set up our router guards. To do this we can simply create a promise the resolves or rejects once the onAuthStateChanged function has been called.

import { getAuth, onAuthStateChanged } from 'firebase/auth'
export const getUserState = () =>
new Promise((resolve, reject) =>
onAuthStateChanged(getAuth(), resolve, reject)
)

Vue Routing

In the routes/index.js we'll add our route options for each of the pages I mentioned earlier.

const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { requiresUnauth: true }
},
{
path: '/signup',
name: 'SignUp',
component: () => import('../views/SignUp.vue'),
meta: { requiresUnauth: true }
}
]

To add route guarding we'll first, need to mark each route that we want to guard with a meta property called requiresAuth.

router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const requiresUnauth = to.matched.some(record => record.meta.requiresUnauth)
const isAuth = await getUserState()
if (requiresAuth && !isAuth) next('/login')
else if (requiresUnauth && isAuth) next('/')
else next()
})

Vue Pages

Login

We'll start with login, which will be a simple form that accepts two user inputs. We can call an onsubmit handler and add the .prevent option. We'll grab the username and password from the form which we'll pass into the signInWithEmailAndPassword function and then redirect the user to the home page using the router. If this fails we'll display an alert message to the user.

I would suggest you add better error handling but this should work well for this tutorial.

<script>
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
import { useRouter } from 'vue-router'
export default {
setup() {
const auth = getAuth()
const router = useRouter()
const handleSubmit = async e => {
const { email, password } = e.target.elements
try {
await signInWithEmailAndPassword(auth, email.value, password.value)
router.push('/')
} catch (e) {
alert(e.message)
}
}
return { handleSubmit }
}
}
</script>
<template>
<h1>Login</h1>
<form @submit.prevent="handleSubmit">
<input name="email" placeholder="email" type="email" />
<input name="password" placeholder="password" type="password" />
<button type="submit">Login</button>
</form>
</template>

Signup

The signup is going to be very similar. Let's copy and paste our login page over, change the title and call createUserWithEmailAndPassword.

<script>
import { getAuth, createUserWithEmailAndPassword } from 'firebase/auth'
import { useRouter } from 'vue-router'
export default {
setup() {
const auth = getAuth()
const router = useRouter()
const handleSubmit = async e => {
const { email, password } = e.target.elements
try {
await createUserWithEmailAndPassword(auth, email.value, password.value)
router.push('/')
} catch (e) {
alert(e.message)
}
}
return { handleSubmit }
}
}
</script>
<template>
<h1>Sign Up</h1>
<form @submit.prevent="handleSubmit">
<input name="email" placeholder="email" type="email" />
<input name="password" placeholder="password" type="password" />
<button type="submit">Register</button>
</form>
</template>

Home

For the home page we'll dispaly a welcome message with the users email. We can get the user using the useAuthState hook we created eailer. Lets also add a signout button, that when called will sign the user out and redirect them to the login page.

<script>
import { getAuth, signOut } from 'firebase/auth'
import { useAuthState } from '../firebase'
import { useRouter } from 'vue-router'
export default {
name: 'Home',
setup() {
const { user } = useAuthState()
const auth = getAuth()
const router = useRouter()
const signOutUser = async () => {
try {
await signOut(auth)
router.push('/login')
} catch (e) {
alert(e.message)
}
}
return { user, signOutUser }
}
}
</script>
<template>
<h1>Welcome {{ user?.email }}!</h1>
<button @click="signOutUser">Sign Out</button>
</template>

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 in-depth content.

Membership

Become a member and gain access to premium content.

Coding With Justin

© 2021 Justin Brooks. All rights reserved.