Firebase & React Part 2: User Authentication

This is a follow up to the CSS-Tricks Article Intro to Firebase and React. In that lesson, we built Fun Food Friends, an application for planning your next potluck. It looked like this:

If you haven't completed that article yet, please complete that article first before attempting this one - it builds on the existing code from that application.

If you'd like to skip that article and dive right into this one, you can clone this repository which contains the finished version of the application from part one. Just don't forget that you'll need to create your own firebase database and swap in the credentials for that one, as well as run npm install before beginning! If you aren't sure how to do either of these things, take a look at part one before diving into this one.

Article Series:

  1. Intro to Firebase and React
  2. User Authentication (You are here!)

What we'll be making

Today we'll be adding authentication to our Fun Food Friends app, so that only users that are signed in can view who is bringing what to the potluck, as well as be able to contribute their own items. When signed out, it will look like this:

When users are not signed in, they will be unable to see what people are bringing to the potluck, nor will they be able to add their own items.

When signed in, it will look like this:

Your name will be automatically added to the Add Item section, and your Google photo will appear in the bottom right-hand corner of the screen. You will also only be able to remove items you added to the potluck.

Before we Start: Get the CSS

I've added some additional CSS to this project in order to give a little bit of polish to the app. Grab it from here and paste it right into `src/App.css`!

Getting Started: Enabling Google Authentication on our Firebase Project

Start by logging in to Firebase Console and visiting your database's Dashboard. Then click on the Authentication tab. You should see something that looks like this:

Click on the Sign-In Method tab:

Firebase can handle authentication by asking the user for an email and password, or it can take advantage of third-party providers such as Google and Twitter in order to take care of authentication and authentication flow for you. Remember when you first logged in to Firebase, it used your Google credentials to authenticate you? Firebase allows you to add that feature to apps that you build.

We're going to user Google as our authentication provider for this project, primarily because it will make handling our authentication flow very simple: we won't have to worry about things like error handling and password validation since Google will take care of all of that for us. We also won't have to build any UI components (other than a login and logout button) to handle auth. Everything will be managed through a popup.

Hover over Google, select the pencil on the right hand side of the screen, and click Enable in the box that appears. Finally, hit Save.

Now, click Database on the left hand side of the screen, and head to the rules panel. It should look something like this right now:

In the first iteration of our fun food friends app, anyone could read and write to our database. We're going to change this so that only users that are signed in can write to the database. Change your rules so that it looks like this, and hit Publish:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

These rules tell Firebase to only allow users who are authenticated to read and write from the database.

Preparing Our App to Add Authentication

Now we're going to need to go back to our `firebase.js` file and update our configuration so that we'll be able to use Google as our third party authentication Provider. Right now your `firebase.js` should look something like this:

import firebase from 'firebase'
const config = {
    apiKey: "AIzaSyDblTESEB1SbAVkpy2q39DI2OHphL2-Jxw",
    authDomain: "fun-food-friends-eeec7.firebaseapp.com",
    databaseURL: "https://fun-food-friends-eeec7.firebaseio.com",
    projectId: "fun-food-friends-eeec7",
    storageBucket: "fun-food-friends-eeec7.appspot.com",
    messagingSenderId: "144750278413"
};
firebase.initializeApp(config);
export default firebase;

Before the export default firebase, add the following:

export const provider = new firebase.auth.GoogleAuthProvider();
export const auth = firebase.auth();

This exports the auth module of Firebase, as well as the Google Auth Provider so that we'll be able to use Google Authentication for sign in anywhere inside of our application.

Now we're ready to start adding authentication! Let's head over to `app.js`. First, let's import the auth module and the Google auth provider so that we can use them inside of our app component:

Change this line:

import firebase from './firebase.js';

to:

import firebase, { auth, provider } from './firebase.js';

Now, inside your App's constructor, let's start by carving out a space in our initial state that will hold all of our signed in user's information.

class App extends Component {
  constructor() {
    super();
    this.state = {
      currentItem: '',
      username: '',
      items: [],
      user: null // <-- add this line
    }

Here we set the default value of user to be null because on initial load, the client has not yet authenticated with Firebase and so, on initial load, our application should act as if they are not logged in.

Adding Log In and Log Out

Now, let's add a log in and log out button to our render component so that the user has some buttons they can click to log in to our application:

<div className="wrapper">
  <h1>Fun Food Friends</h1>
  {this.state.user ?
    <button onClick={this.logout}>Log Out</button>                
    :
    <button onClick={this.login}>Log In</button>              
  }
</div>

If the value of user is truthy, then it means that the user is currently logged in and should see the logout button. If the value of user is null, it means that the user is currently logged out and should see the log in button.

The onClick of each of these buttons will point to two functions that we will create on the component itself in just a second: login and logout .

We'll also need to bind these functions in our constructor, because eventually we will need to call this.setState inside of them and we need access to this:

constructor() {
  /* ... */
  this.login = this.login.bind(this); // <-- add this line
  this.logout = this.logout.bind(this); // <-- add this line
}

The login method, which will handle our authentication wth Firebase, will look like this:

handleChange(e) {
  /* ... */
}
logout() {
  // we will add the code for this in a moment, but need to add the method now or the bind will throw an error
}
login() {
  auth.signInWithPopup(provider) 
    .then((result) => {
      const user = result.user;
      this.setState({
        user
      });
    });
}

Here we call the signInWithPopup method from the auth module, and pass in our provider (remember this refers to the Google Auth Provider). Now, when you click the 'login' button, it will trigger a popup that gives us the option to sign in with a Google account, like this:

signInWithPopup has a promise API that allows us to call .then on it and pass in a callback. This callback will be provided with a result object that contains, among other things, a property called .user that has all the information about the user who has just successfully signed in - including their name and user photo. We then store this inside of the state using setState.

Try signing in and then checking the React DevTools - you'll see the user there!

It's you! This will also contain a link to your display photo from Google, which is super convenient as it allows us to include some UI that contains the signed in user's photo.

The logout method is incredibly straightforward. After the login method inside your component, add the following method:

logout() {
  auth.signOut()
    .then(() => {
      this.setState({
        user: null
      });
    });
}

We call the signOut method on auth, and then using the Promise API we remove the user from our application's state. With this.state.user now equal to null, the user will see the Log In button instead of the Log Out button.

Persisting Login Across Refresh

As of right now, every time you refresh the page, your application forgets that you were already logged in, which is a bit of a bummer. But Firebase has an event listener, onAuthStateChange, that can actually check every single time the app loads to see if the user was already signed in last time they visited your app. If they were, you can automatically sign them back in.

We'll do this inside of our componentDidMount, which is meant for these kinds of side affects:

componentDidMount() {
  auth.onAuthStateChanged((user) => {
    if (user) {
      this.setState({ user });
    } 
  });
  // ...

When the user signs in, this checks the Firebase database to see if they were already previously authenticated. If they were, we set their user details back into the state.

Updating the UI to Reflect the User's Login

Now that our user's authentication details are being tracked successfully in our application's state and synced up with our Firebase database, there's only one step left - we need to link it up to our application's UI.

That way, only signed in users see the potluck list and have the ability to add new items. When a user is logged in, we see their display photo, their name is automatically populated into the 'Add Item' area, and they can only remove their own potluck items.

I want you to start by erasing what you previously had after the <header> inside of your app's render method - it'll be easier to add back each thing at a time. So your app component's render method should look like this.

render() {
  return (
    <div className='app'>
      <header>
        <div className="wrapper">
          <h1>Fun Food Friends</h1>
          {this.state.user ?
            <button onClick={this.logout}>Logout</button>                
          :
            <button onClick={this.login}>Log In</button>              
          }
        </div>
      </header>
    </div>
  );
}

Now we're ready to start updating the UI.

Show the User's Photo if Logged In, Otherwise Prompt User to Log In

Here we're going to wrap our application in a big old' ternary. Underneath your header:

<div className='app'>
  <header>
    <div className="wrapper">
      <h1>Fun Food Friends</h1>
      {this.state.user ?
        <button onClick={this.logout}>Logout</button>                
        :
        <button onClick={this.login}>Log In</button>              
      }
    </div>
  </header>
  {this.state.user ?
    <div>
      <div className='user-profile'>
        <img src={this.state.user.photoURL} />
      </div>
    </div>
    :
    <div className='wrapper'>
      <p>You must be logged in to see the potluck list and submit to it.</p>
    </div>
  }
</div>

Now, when you click login, you should see this:

Show the Add Item Area and Pre-populate with the Signed in User's Login Name or E-mail

<div>
  <div className='user-profile'>
     <img src={this.state.user.photoURL} />
  </div>
  <div className='container'>
    <section className='add-item'>
      <form onSubmit={this.handleSubmit}>
        <input type="text" name="username" placeholder="What's your name?" value={this.state.user.displayName || this.state.user.email} />
        <input type="text" name="currentItem" placeholder="What are you bringing?" onChange={this.handleChange} value={this.state.currentItem} />
        <button>Add Item</button>
      </form>
    </section>
  </div>
</div>

Here we set the value of our username field to this.state.user.displayName if it exists (sometimes users don't have their display name set), and if it doesn't we set it to this.state.user.email. This will lock the input and make it so that user's names or email are automatically entered into the Add Item field for them.

We'll also update the handleSubmit since we no longer rely on handleChange to set the user's name in the state, but can grab it right off of this.state.user:

handleSubmit(e) {
  // ....
  const item = {
    title: this.state.currentItem,
    user: this.state.user.displayName || this.state.user.email
  }
  // ....
}

Your app should now look like this:

Displaying Potluck Items, and Giving the User the Ability to Only Remove Their Own

Now we'll add back our list of potluck items. We'll also add a check for each item to see if the user who is bringing the item matches the user who is currently logged in. If it does, we'll give them the option to remove that item. This isn't foolproof by far and I wouldn't rely on this in a production app, but it's a cool little nice-to-have we can add to our app:

<div className='container'>
  {/* .. */}
  <section className='display-item'>
    <div className="wrapper">
      <ul>
        {this.state.items.map((item) => {
          return (
            <li key={item.id}>
              <h3>{item.title}</h3>
              <p>brought by: {item.user}
                 {item.user === this.state.user.displayName || item.user === this.state.user.email ?
                   <button onClick={() => this.removeItem(item.id)}>Remove Item</button> : null}
              </p>
            </li>
          )
        })}
      </ul>
    </div>
  </section>
</div>

Instead of displaying the remove button for each item, we write a quick ternary that checks to see if the person who is bringing a specific item matches the user who is currently signed in. If there's a match, we provide them with a button to remove that item:

Here I can remove Pasta Salad, since I added it to the potluck list, but I can't remove potatoes (who brings potatoes to a potluck? My sister, apparently.)

And that's all there is to it! Adding authentication to a new (or existing) Firebase application is a snap. It's incredibly straightforward, can be added with minimal refactoring, and allows to persist authentication across page refresh.

It's important to note that this is a trivial application - you would want to add additional checks and balances for storing any kind of secure information. But for our application's simple purposes, it's a perfect fit!

Article Series:

  1. Intro to Firebase and React
  2. User Authentication (You are here!)