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.
Justin Brooks
May 23, 2021
Firebase Authentication Tutorial with Private Routes in React and Vue
FirebaseReactReact Project SetupReact RoutingReact PagesVueVue Project SetupVue RoutingVue PagesConclusionAuthentication 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.
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.
Let's get started with the most popular option, React.
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
npm i react-router 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.
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.
Now, let's go through each page and those up.
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.elementsconst 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.
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.elementsconst 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></>)}
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></>)}
This takes care of our react integration now let's move on to my personal favorite, Vue.
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
npm i 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 unsubscribeonMounted(() => {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))
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()})
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.elementstry {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>
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.elementstry {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>
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>
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.
Become a member and gain access to premium content.