In this article, we are going to make a basic user auth with firebase. If you have experience with any other type of user auth, you probably got frustrated.
Firebase does have a learning curve but I have found it small compared to other alternatives.
Firebase is going to do a lot of the heavy backend functionality
If you would like to see what this app does here is the "finished" product you can here
Why is this tutorial useful?
This is how to leverage firebase so that you don't have to create your own backend, encrypt your user's passwords or go through the hassle of deploying a backend application.
Prerequisites:
understanding of JavaScript including how to pass arguments to functions and asynchronous code.
understanding of react, context, hooks with create-react-app.
Text editor of your choice.(I will use vscode)
basic understanding of the command line.
knowledge of git.
Optional: bash command line/Mac OS. You can do this without it but I’ll be using it for this tutorial.
first, make a new firebase project by visiting https://firebase.com.
Click on a new project.
click "my first project" and then you can name your project whatever you want.
Click continue.
You can choose not to have google analytics and it should not interfere with this tutorial, I left it on, so you will see parts of my code where it’s enabled.
Click continue.
You will be prompted to select an account.
select the default account, then click create project.
you should now see this.
you should be in your firebase console for this project.
click on authentication on the left side navigation.
click set up sign-in method.
here is a wide variety of ways to set up users signing into our apps. We are going to do the easiest way for this tutorial.
click email and password.
Click enable.
Save.
Make sure that it actually got enabled.
Now go to the project overview.
We need to get info about how our app can send and receive firebase data, so we have to get API keys and other sensitive information given to us in the form of an SDK.
Click on the brackets to begin.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zzpeg5dqj7qmlewy87h9..
We will be creating a react app and adding everything inside the script tag to the react project.
since we don't have a firebaseIndex.js we can't add it yet.
this is everything we have to do on the firebase console for our project.
make a new react app.
create-react-app firebaseauthtutorial
cd the app
cd firebaseauthtutorial
this is a good moment to plan out what kind of packages are wanted. these will all be installed via npm.
firebase. if this was an ordinary javascript, we would use the whole script take and the SKD.
this is so that when a user logs in we display components only accessible by users.
- dotenv, the best habit you can have with making apps that contain user data or leveraging APIs' (like this app will) is to ensure that hackers can't get access to your API keys, encryption techniques or other users sensitive info.
dotenv allows you to save sensitive information as environment wide variables, in a way that you can't publish to a remote repo but still be able to use in your app.
run an npm install on the command line for all the packages
pro tip: make sure that you are in the root directory of the project before you run npm install
npm install firebase dotenv react-router-dom
now open the project.
I'm using vscode so this is how from the command line.
code .
look at the package.json file and you should see the packages that you installed.
moving SDK firebase in the app.
before you copy and paste the SDK into our file, its best practice to add the .env file to the .gitignore so that you don't publish your environment variables to github. It is very easy to forget.
then add the API keys to the .env
then reference them from the firebaseIndex.js we are about to create to the .env file.
this way, you are never in danger of publishing your keys while following this tutorial.
Click on your .gitignore
write .env anywhere in the file
then right-click a blank spot in the root directory. (if you don't have one you can minimize the outline to reveal space.)
copy and paste the following variables to the .env file
REACT_APP_API_KEY=
REACT_APP_AUTHDOMAIN=
REACT_APP_BASEURL=
REACT_APP_PROJECT_ID=
REACT_APP_STORAGEBUCKET=
REACT_APP_MESSAGING_SENDER_ID=
REACT_APP_APP_ID=
REACT_APP_MEASUREMENT_ID=
Including the quotations copy and paste the info from the SDK one by one. API key, auth domain, baseurl ect...
you should have something like this.
your info from firebase.
REACT_APP_API_KEY="your secret api key"
REACT_APP_AUTHDOMAIN="your secret authdomain"
REACT_APP_BASEURL="your secret baseurl"
REACT_APP_PROJECT_ID="your secret projectid"
REACT_APP_STORAGEBUCKET="your secret storeagebucket"
REACT_APP_MESSAGING_SENDER_ID="your secret messaging sender id"
REACT_APP_APP_ID="your secret app id"
REACT_APP_MEASUREMENT_ID="your secret measurment id"
now the easy part.
Begin by making the folder to keep firebases SDK and the helper methods for the auth.
try and do this from your text editor.
by right-clicking the src folder and click new folder.
name the folder firebase.
now right-click the firebase folder and add a firebaseIndex.js
firebaseIndex.js.
import firebase at the top of the firebaseIndex.js file along with the features you want from it.
import firebase from 'firebase'
import 'firebase/auth'
import 'firebase/app'
now that your environment variables are already set up app-wide you can copy and paste this SDK to reference your sensitive data inside the firebaseIndex file with the code I provide.
var firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTHDOMAIN,
databaseURL: process.env.REACT_APP_BASEURL,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGEBUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENT_ID
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
add firebase.auth() helper method underneath the analytics() method.
firebase.auth()
we are going to need the firebaseConfig object in another file so it needs to be exported
export default {
firebaseConfig,
}
the whole file should look like this.
import firebase from 'firebase'
import 'firebase/auth'
import 'firebase/app'
var firebaseConfig = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTHDOMAIN,
databaseURL: process.env.REACT_APP_BASEURL,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGEBUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENT_ID
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
firebase.auth()
export default {
firebaseConfig,
}
if you followed these steps you could have pushed to github at any time and it would not have saved your keys.
Adding the auth methods.
inside your firebase folder make a file called auth methods, this is where to keep an object that contains the signin, signup, signout, functions.
at the top import two things, firebaseConfig object and firebase from firebase like so.
import firebaseconfig from './firebaseIndex'
import firebase from 'firebase'
now make an export and make an auth methods object.
export const authMethods = {
// firebase helper methods go here...
}
we are going to send this to context where this will be the top of a chain of methods that link all the way to the form for signin.
these are going to be key/value pairs that we give anonymous functions for signing in.
export const authMethods = {
// firebase helper methods go here...
signup: (email, password) => {
},
signin: (email, password) => {
},
signout: (email, password) => {
},
}
this looked really unusual the first time I saw it. This will make a lot more sense after we start calling on it from context.
this is from the firebase documentation for user auth.
signup: (email, password) => {
firebase.auth().createUserWithEmailAndPassword(email,password)
.then(res => {
console.log(res)
})
.catch(err => {
console.error(err)
})
},
I want to test if this code works before I start adding the other methods.
to do that build the context and signup form and see if firebase will respond.
Creating context for our application.
right-click on the src folder and make a new folder called provider.
right-click on provider and make a file called AuthProvider.js
make a functional component, add props.
import React from 'react';
const AuthProvider = (props) => {
return (
<div>
</div>
);
};
export default AuthProvider;
outside of the function, make a firebaseAuth variable and make it equal to react context.
export const firebaseAuth = React.createContext()
we have to export it so that we can access the useContext hook.
erase the div tags and make the provider inside of the return for the AuthProvider I'm not going to explain everything that is happening here but if you want to know more about context this is an article where I explain context and the useContext hook.
const AuthProvider = (props) => {
return (
<firebaseAuth.Provider
value={{
test: "context is working"
}}>
{props.children}
</firebaseAuth.Provider>
);
};
AuthProvider.js
now we need to wrap our App.js in the AuthProvider component in the index.js file.
we also need to import our ability to route components dynamically, since we are already in this file, add BrowserRouter from react-router-dom.
start by importing the AuthProvider and BrowserRouter at the top.
import AuthProvider from './provider/AuthProvider'
import {BrowserRouter} from 'react-router-dom'
then make an App sandwich with BrowserRouter and AuthProvider.
ReactDOM.render(
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
, document.getElementById('root'));
two things,
go to the App.js, at the top change how react is imported to include useContext and React.
import {firebaseAuth} so that we can destructure the test key/value pair out of it like this.
import React, {useContext} from 'react';
import {firebaseAuth} from './provider/AuthProvider'
inside the function destructure test from the firebaseAuth variable.
console.log test.
const {test} = useContext(firebaseAuth)
console.log(test)
go back to the terminal and start the server.
npm start
inspect with the dev tools and you should see this.
connecting to authMethods
now that we have context App wide, go back to the AuthProvider.js and import the authMethods.
import {authMethods} from '../firebase/authmethods'
This file to be the middle man between firebase and the Signup component we are about to make,
that means all the stateful logic will be housed here.
make a function called handleSignup inside the AuthProvider.
const handleSignup = () => {
// middle man between firebase and signup
}
Pass it as a value in the firebaseAuth.Provider
<firebaseAuth.Provider
value={{
//replaced test with handleSignup
handleSignup
}}>
{props.children}
</firebaseAuth.Provider>
now change test with handleSignup in the App.js
const {handleSignup} = useContext(firebaseAuth)
console.log(handleSignup)
App.js
you should see
in the AuthProvider, add the authMethod.signup() to the handleSignup.
const handleSignup = () => {
// middle man between firebase and signup
console.log('handleSignup')
// calling signup from firebase server
return authMethods.signup()
}
make a components folder and Signup.js component, recreate the same functionality where we want it to end up so that we can define our routing in the App.js
make the Signup.js
make a basic component
// add useContext
import React, {useContext} from 'react';
const Signup = () => {
return (
<div>
Signup
</div>
);
};
export default Signup;
destructure the handleSignup function out of context just like in the App.js
const {handleSignup} = useContext(firebaseAuth)
console.log(handleSignup)
__
in the App.js add the beginnings of react-router-dom by removing the boilerplate and adding Switch and Route, setting the signup to be rendered by the Route.
import {Route, Switch} from 'react-router-dom'
import Signup from './component/Signup'
App.js
return (
<>
{/* switch allows switching which components render. */}
<Switch>
{/* route allows you to render by url path */}
<Route exact path='/' component={Signup} />
</Switch>
</>
);
if everything worked you should see a white screen with signup.
make a signup form.
return (
<form>
{/* replace the div tags with a form tag */}
Signup
{/* make inputs */}
<inputs />
<button>signup</button>
</form>
);
at this point, it might be tempting to make state here. but we want the context to be the single source of truth so that if a user toggles between login and signup, whatever they typed in will persist.
go back to the AuthProvider and start setting up state.
we need a piece of state for a token from firebase and for user data.
import useState next to React.
import React, {useState} from 'react';
AuthProvider.js
the pieces of state that we want will be.
token as null (then a string once we get a token from firebase), more about json web tokens.
input as an object with email and password both strings.
errors as an array, so that error messages can be displayed to the users.
add those states to the AuthProvider.js
const [inputs, setInputs] = useState({email: '', password: ''})
const [errors, setErrors] = useState([])
const [token, setToken] = useState(null)
add inputs to the value object of the provider.
<firebaseAuth.Provider
value={{
//replaced test with handleSignup
handleSignup,
inputs,
setInputs,
}}>
in the Signup.js get them from the authContext with the useContext hook like this.
const {handleSignup, inputs, setInputs} = useContext(firebaseAuth)
make handleChange and handleSubmit functions as basic forms.
const handleSubmit = (e) => {
e.preventDefault()
console.log('handleSubmit')
}
const handleChange = e => {
const {name, value} = e.target
console.log(inputs)
setInputs(prev => ({...prev, [name]: value}))
}
change the form and input fields to work with the form functions.
<form onSubmit={handleSubmit}>
{/* replace the div tags with a form tag */}
Signup
{/* make inputs */}
<input onChange={handleChange} name="email" placeholder='email' value={inputs.email} />
<input onChange={handleChange} name="password" placeholder='password' value={inputs.password} />
<button>signup</button>
</form>
if you did everything correctly and ran a test that looks like this...
here is the error message you would have gotten.
the reason we got this error is that we didn't pass the authMethods.signup the email and password arguments that it was expecting.
pass inputs.email and inputs.password into authMethods.signin
authMethods.signup(inputs.email, inputs.password)
when you do a test like this.
you should get a response like this.
but if you try and do it twice you will get an error.
this is because you can't do this twice. all of the emails have to be unique.
to make it so that the error message displays to the user we have to do the following.
- in the AuthProvider.js, pass setErrors as an argument along with email and password,
this is the only way I could figure out how to do this. whenever you do have to pass more than one argument to a function you should have a good justification.
in the authMethods.js on the signup(), add the third argument at the top and in the .catch, we will have the error messages save to state in the errors array.
have the error display to the screen by passing it to the Signup.js and mapping through the array.
1.
//sending setErrors
authMethods.signup(inputs.email, inputs.password, setErrors)
console.log(errors)
now add the setErrors message along with email and password.
AuthProvider.js
2.
//catching setErrors
signup: (email, password, setErrors) => {
authMethods.js
change the catch to the setErrors include prev in case it's more than one error
.catch(err => {
//saving error messages here
setErrors(prev => ([...prev, err.message]))
})
if it worked and you console logged it, you should see this error.
- add errors to the value object of the Provider
<firebaseAuth.Provider
value={{
//replaced test with handleSignup
handleSignup,
inputs,
setInputs,
//added errors to send to Signup.js
errors,
}}>
{props.children}
</firebaseAuth.Provider>
AuthProvider.js
destructure it from useContext from the Signup.js
const {handleSignup, inputs, setInputs, errors} = useContext(firebaseAuth)
Signup.js
now add a ternary that will only show up if an error occurs.
<button>signup</button>
{errors.length > 0 ? errors.map(error => <p style={{color: 'red'}}>{error}</p> ) : null}
</form>
if everything worked you will get your error on the screen.
if you want to filter duplicates you can find out or see how I did on the repo but this tutorial is getting long and a couple more things to do.
to make it so that you can enable multiple emails per account.
go to firebase inside of this project, click on authentication.
click on signin method
scroll to the bottom and where it says advanced in small black letters. it says one account per email in bold.
Click the blue change button
click allow multiple accounts with the same email.
this will help us move faster with testing but don't forget to switch it back later.
The same way that we set an error we are going to save the token to localStorage and the token's state in the AuthProvider.
make it so that we can only see some components if we have a token.
redirect to that page if the token in local storage matches the token in state.
repeat the process for signin.
erase the token and pushing the user out of the authenticated parts of our app with the login method.
go to the AuthProvider.js and add setToken as another argument after setErrors.
//sending setToken function to authMethods.js
authMethods.signup(inputs.email, inputs.password, setErrors, setToken)
console.log(errors, token)
AuthProvider.js
add this as a 4th argument at the top.
// added the 4th argument
signup: (email, password, setErrors, setToken) => {
inside the .then, underneath the console.log(res)...
I am about to save you so much time you would have to spend digging through the res object to find the token.
this is also about to be a little messy with the async code.
signup: (email, password, setErrors, setToken) => {
firebase.auth().createUserWithEmailAndPassword(email,password)
//make res asynchronous so that we can make grab the token before saving it.
.then( async res => {
const token = await Object.entries(res.user)[5][1].b
//set token to localStorage
await localStorage.setItem('token', token)
//grab token from local storage and set to state.
setToken(window.localStorage.token)
console.log(res)
})
.catch(err => {
setErrors(prev => ([...prev, err.message]))
})
},
authMethods.js
now if you make yet another account and go to the browsers dev tools
_2. signing in _
we are going to copy and paste a lot of what we have for signup and easily configure it for login.
we will start from the bottom of the component tree by making a Signin component slightly change file by file until it works in the authMethods.
start by making a new file called Signin.js
copy and paste everything from the Signup.js to the Signin.js
highlight everywhere it says signup and change that to signin
Click on the name of the react component and Command + d if you are using a mac. Otherwise, you can use ctrl + f and type it in at the top.
I only had 3 words by remember to change handleSignup to handleSignin using the same method.
change the button as well.
Now go to the App.js and import the file.
import Signin from './component/Signin'
make sure that component folder on the import is singular.
add a new route for the Signin
<Route exact path='/' component={Signup} />
<Route exact path='/signin' component={Signin} />
your signin component will render now if you type in the http://localhost:3000/signin but as soon as you do click the button it will crash because there is no handleSignin function.
to fix that we can go to the AuthProvider.js and copy and paste changing the wording just like we did for signup. then add the handleSignin function to the value object.
const handleSignin = () => {
//changed to handleSingin
console.log('handleSignin!!!!')
// made signup signin
authMethods.signin(inputs.email, inputs.password, setErrors, setToken)
console.log(errors, token)
}
now to add that function to the firebaseAuth.Provider
<firebaseAuth.Provider
value={{
//replaced test with handleSignup
handleSignup,
handleSignin,
inputs,
setInputs,
errors,
}}>
{props.children}
</firebaseAuth.Provider>
AuthProvider.js
now go to authMethods.js and do something similar, instead of createUserWithEmailAndPassword, change to... signInWithEmailAndPassword()
signin: (email, password, setErrors, setToken) => {
//change from create users to...
firebase.auth().signInWithEmailAndPassword(email,password)
//everything is almost exactly the same as the function above
.then( async res => {
const token = await Object.entries(res.user)[5][1].b
//set token to localStorage
await localStorage.setItem('token', token)
setToken(window.localStorage.token)
console.log(res)
})
.catch(err => {
setErrors(prev => ([...prev, err.message]))
})
},
if you didn't delete your token from local storage then a token will still be there.
almost there!!
make a home component and only allow users with tokens to get there.
make a signout button that deletes the token and pushes the user away from the page with react-router-dom.
since you should already be in the authMethods.js we will start from the top and go to the bottom this time.
this method is really simple compared to the other two because we aren't using firebase to keep user's status there.
//no need for email and password
signout: (setErrors, setToken) => {
// signOut is a no argument function
firebase.auth().signOut().then( res => {
//remove the token
localStorage.removeItem('token')
//set the token back to original state
setToken(null)
})
.catch(err => {
//there shouldn't every be an error from firebase but just in case
setErrors(prev => ([...prev, err.message]))
//whether firebase does the trick or not i want my user to do there thing.
localStorage.removeItem('token')
setToken(null)
console.error(err.message)
})
},
}
go to AuthProvider.js and make a signout function
const handleSignout = () => {
authMethods.signout()
}
add the method to the Provider
setInputs,
errors,
handleSignout,
now we need a component for this to be useful which we haven't done yet.
make a Home.js, and a basic React component inside it.
import React from 'react';
const Home = (props) => {
return (
<div>
Home
</div>
);
};
export default Home;
import useContext and firebaseAuth
import React, {useContext} from 'react';
import {firebaseAuth} from '../provider/AuthProvider'
between return and Home inside the Component, destructure signout from useContext
const {signout,} = useContext(firebaseAuth)
in the return statement. add login successful, then a button to call on signout.
return (
<div>
Home, login successful!!!!!!
<button onClick={signout}>sign out </button>
</div>
);
before we can test it, we need to go back up our component tree and change how strict it is to access each component.
in the App.js we are going to use a ternary statement to make it so that users can't get to the home component without a token saved to state.
import the Home component in the App.js.
import Home from './component/Home'
destructure the token out of firebaseAuth with useContext
const { token } = useContext(firebaseAuth)
console.log(token)
when you use Route to render the Home component, add a ternary statement checking the data type of the token
this means that setting up the "/" or root URL differently.
change your Home components route to use the render prop instead of the component prop. and designate the URL paths more strictly.
<Route exact path='/' render={rProps => token === null ? <Signin /> : <Home />} />
<Route exact path='/signin' component={Signin} />
<Route exact path='/signup' component={Signup} />
in the AuthProvider.js, add the token to the value object.
<firebaseAuth.Provider
value={{
//replaced test with handleSignup
handleSignup,
handleSignin,
token,
inputs,
setInputs,
errors,
handleSignout,
}}>
{props.children}
</firebaseAuth.Provider>
now users can signin and signout. One final touch, make it so that when a user signs up, react-router-dom will send them to the home page.
go to the Signup.js and import withRouter from react-router-dom
import {withRouter} from 'react-router-dom'
pass the default export to the withRouter higher-order component
export default withRouter(Signup);
add props to the Signup component
const Signup = (props) => {
now we have access to prop.history.push("/goAnyWhereInApp")
now make handleSubmit an async function and await the handleSignup and then push to the root URL.
const handleSubmit = async (e) => {
e.preventDefault()
console.log('handleSubmit')
//wait to signup
await handleSignup()
//push home
props.history.push('/')
}
you might have a delay, but once you get your credentials it will work.
if you want to publish this sight here is how with surge. I am a big fan and am doing these firebase tutorials because of a dev who has suffered much at the hands of heroku
this is the finished product
this is the github give it a star if you can.
Finally, that is it
you now have a static site with powerful backend functionality.
I will be doing a lot more tutorials on firebase.
please like and share if you found this tutorial beneficial.
the firebase docs are helpful but I have a few things in here that make it so much easier to transpose to a react project.
if you have anything to say please add it to the comments below.
Top comments (22)
Hey Tallan, thank you for helping me get my User Auth set up! I have a quick question, Whenever I refresh the page, my token leaves local storage and the user is signed out. Is there a function I can employ in the authMethod.js file to prevent this from happening? Thanks!
I can’t help you out for a couple weeks, I’m very sorry, I’m currently on vacation.
I will do some research as soon as I can and I really appreciate you taking the time to read my tutorial and asking questions but I cannot answer them in a timely manner.
Hey, Tallan. Thanks for a nice tutorial.
When I came to the part where you store the token in localStorage, I couldn't find the token at the same place in the res Object. I then started Googling this and it seems firebase automatically stores the user's credentials in local storage, and reloads it from there when the app restarts/page reloads, under Storage -> IndexedDB.
If the response object has changed since you created this tutorial, what would you suggest I do to move forward?
I’m going to have to look at the res object from firebase and all of application storage in order to give you the best answer, but I did notice that the token was in the indexDB when I wrote this tutorial. I found it easier to dig through object from firebase and save it to local storage rather than try and access indexDB. My recommendation is the dig through the object and save the token to local storage, however it is worth assessing the difficulty of cutting out the middle man and just using indexDB.
I will give you a better answer than this when I have the time to look at the code again.
Wow, that's fantastic. I'm looking forward to hear what you find.
I can see that some people recommend useEffect and something called onAuthStateChanged, like so:
I guess this is using the stored data in the indexedDB(?), but if you would consider this as a viable option - how would you recommend integrating this with the context? If you could comment on this as well, it would be really awesome!
Thanks Tallan for the amazing tutorial. I ran into some errors trying to reproduce it in my react project.
"TypeError: Cannot destructure property 'token' of 'Object(...)(...)' as it is undefined."
Please could you look at this?
try checking the way you are importing firebaseAuth ,
Import like this
import { firebaseAuth } from './../provider/AuthProvider';
instead of like this
import firebaseAuth from './../provider/AuthProvider';
I got the same error, this how I solved it
Hi Tallan thanks for tutorial, it's brilliant. There are two errors in the code above.
You fixed them in your source repo but if someone is stuck, it may help.
const {signout,} = useContext(firebaseAuth)
-> signout is need to be replaced by handleSignout.
const handleSignout = () => {
authMethods.signout()
}
-> we need to pass setErrors and setToken as parameters to authMethods.signout().
can you use:
const token = await res.user.refreshToken
instead of:
const token = await Object.entries(res.user)[5][1].b
?
for those having issues of session lost while refreshing.
add this in AuthProvider.js
const [token,setToken] = useState(localStorage.getItem("token"));
Great fix. My build was stuck that it has already logged in. This corrected it.
Try this
Just enable FCM in your google cloud console,
More Stackoverflow: stackoverflow.com/questions/600060...
Yes that would be really cool. How do I do that?
I get the following, anyone else come up with this problem?
TypeError: Cannot destructure property 'handleSignin' of 'Object(...)(...)' as it is undefined.
const SignIn = () => {
const {handleSignin, inputs, setInputs, errors} = useContext(firebaseAuth)
const handleSubmit = (e) => {
e.preventDefault()
thank you so much for such a nice tutorial its working