DEV Community

Cover image for React Firebase Authentication
Mahmoud Elmahdi
Mahmoud Elmahdi

Posted on

React Firebase Authentication

We're going to build a simple authentication and secure application in React and Firebase Authentication SDKs. Users will have the ability to create account, sign in and sign out. We'll also make certain routes (private pages) secure and protected to be only used by authenticated users. I hope you find it helpful!

Application Installation & Setup

To get started we'll create an application that will be bootstrapped with Facebook’s official React boilerplate create-react-app.

Run npm i -g create-react-app to have it installed on your local machine

# Creating an App
  create-react-app react-firebase-auth
# Change directory
  cd react-firebase-auth
# Additional packages to install
  yarn add firebase react-router-dom react-props
Enter fullscreen mode Exit fullscreen mode

The initial project structure is now generated, and all dependencies installed successfully. Let's rock up our project hierarchy and it's folder structure as showing below:

# Make sub-directories under src/ path
  mkdir -p src/{components,firebase,shared}
# Move App component under src/components
  mv src/App.js src/components
# Create desired files to work with
  touch src/components/{AppProvider,Navbar,FlashMessage,Login,Signup}.js
  touch src/firebase/{firebase,index,auth,config}.js
  touch src/shared/Form.js
Enter fullscreen mode Exit fullscreen mode

We need to make sure that everything fall into place by listing all created files, and sub-directories via command line cd src/ && ls * -r

# terminal

react-firebase-auth % cd src/ && ls * -r
components:
App.js   AppProvider.js  FlashMessage.js Login.js  Navbar.js  Signup.js

firebase:
auth.js  config.js   firebase.js index.js

shared:
Form.js
Enter fullscreen mode Exit fullscreen mode

Firebase

We're not going to deep dive into Firebase itself.
If you're not familiar with Firebase please make sure you check their guide
on how to Add Firebase to your JavaScript Project

Firebase configuration

// src/firebase/config.js

const devConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "AUTH_DOMAIN",
  databaseURL: "DATABASE_URL",
  projectId: "PROJECT_ID",
  storageBucket: "STORAGE_BUCKET",
  messagingSenderId: "MESSAGING_SENDER_ID"
};

const prodConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "AUTH_DOMAIN",
  databaseURL: "DATABASE_URL",
  projectId: "PROJECT_ID",
  storageBucket: "STORAGE_BUCKET",
  messagingSenderId: "MESSAGING_SENDER_ID"
};

export {
  devConfig,
  prodConfig
}
Enter fullscreen mode Exit fullscreen mode

Config breakdown

  • devConfig used for development environment
  • prodConfig used for production environment

📌 its alway good to have a config template file for your project with predefined setup (as showing above) to avoid pushing sensitive data to a repository. You or any one of your team can later make a copy of this template with the proper file extension. Example (based on this post): Create a file firebase.config open your .gitignore and add app/config.js then run cp app/firebase.config app/config.js to copy of that config template.

Firebase initialization

// src/firebase/firebase.js

import * as firebase from 'firebase';
import { devConfig } from './config';

!firebase.apps.length && firebase.initializeApp(devConfig);

const auth = firebase.auth();

export {
  auth
}
Enter fullscreen mode Exit fullscreen mode

Auth module

// src/firebase/auth.js

import { auth } from './firebase';

/**
 * Create user session
 * @param {string} action - createUser, signIn
 * @param {string} email 
 * @param {string} password 
 */
const userSession = (action, email, password) => auth[`${action}WithEmailAndPassword`](email, password);

/**
 * Destroy current user session
 */
const logout = () => auth.signOut();

export {
  userSession,
  logout
}
Enter fullscreen mode Exit fullscreen mode

Auth module breakdown

  • userSession a function that accepts three params action: decides whether user creates an account or login, email and password
  • logout destroys the current user session and log the user out of the system

Firebase module

// src/firebase/index.js

import * as auth from './auth';
import * as firebase from './firebase';

export {
  auth,
  firebase
}
Enter fullscreen mode Exit fullscreen mode

Components

Provider component

// src/components/AppProvider.js

import React, {
  Component,
  createContext
} from 'react';
import { firebase } from '../firebase';
export const {
  Provider,
  Consumer
} = createContext();

class AppProvider extends Component {
  state = {
    currentUser: AppProvider.defaultProps.currentUser,
    message: AppProvider.defaultProps.message
  }

  componentDidMount() {
    firebase.auth.onAuthStateChanged(user => user && this.setState({
      currentUser: user
    }))
  }

  render() {
    return (
      <Provider value={{
        state: this.state,
        destroySession: () => this.setState({ 
          currentUser: AppProvider.defaultProps.currentUser 
        }),
        setMessage: message => this.setState({ message }),
        clearMessage: () => this.setState({ 
          message: AppProvider.defaultProps.message 
        })
      }}>
        {this.props.children}
      </Provider>
    )
  }
}

AppProvider.defaultProps = {
  currentUser: null,
  message: null
}

export default AppProvider;
Enter fullscreen mode Exit fullscreen mode

AppProvider breakdown

AppProvider is a React component provides a way to pass data through the component tree without having to pass props down manually at every level and allows Consumers to subscribe to context changes.

  • componentDidMount after a component is mounted, we check against user existence.

Navbar component

// src/components/Navbar.js

import React from 'react';
import {
  Link,
  withRouter
} from 'react-router-dom';
import { auth } from '../firebase';
import { Consumer } from './AppProvider';

const Navbar = props => {
  const handleLogout = context => {
    auth.logout();
    context.destroySession();
    props.history.push('/signedOut');
  };

  return <Consumer>
    {({ state, ...context }) => (
      state.currentUser ?
        <ul>
          <li><Link to="/dashboard">Dashboard</Link></li>
          <li><a onClick={() => handleLogout(context)}>Logout</a></li>
        </ul>
        :
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/login">Login</Link></li>
          <li><Link to="/signup">Create Account</Link></li>
        </ul>
    )}
  </Consumer>
};

export default withRouter(Navbar);
Enter fullscreen mode Exit fullscreen mode

Navbar breakdown

The Navbar component handles UI logic as the following:

  1. If the system logged in user then we show Dashboard (protected page) and the Logout button which kicks out the user and redirect to /signedOut page.
  2. If no users found then we display Home, Login and Create and Account links.

FlashMessage component

// src/components/FlashMessage.js

import React from 'react';
import { Consumer } from '../components/AppProvider';

const FlashMessage = () => <Consumer>
  {({ state, ...context }) => state.message && <small className="flash-message">
    {state.message}
    <button type="button" onClick={() => context.clearMessage()}>Ok</button>
  </small>}
</Consumer>;

export default FlashMessage;
Enter fullscreen mode Exit fullscreen mode

FlashMessage breakdown

FlashMessage is a stateless component wrapped by Consumer that subscribes to context changes. It shows up when something goes wrong (i.e. Form validation, server error, etc...). The FlashMessage has "Ok" button that clears it up and close/hide it.

Form component

// src/shared/Form.js

import React, {
  Component,
  createRef
} from 'react';
import PropTypes from 'prop-types';
import { auth } from '../firebase';

class Form extends Component {
  constructor(props) {
    super(props);

    this.email = createRef();
    this.password = createRef();
    this.handleSuccess = this.handleSuccess.bind(this);
    this.handleErrors = this.handleErrors.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSuccess() {
    this.resetForm();
    this.props.onSuccess && this.props.onSuccess();
  }

  handleErrors(reason) {
    this.props.onError && this.props.onError(reason);
  }

  handleSubmit(event) {
    event.preventDefault();
    const {
      email,
      password,
      props: { action }
    } = this;

    auth.userSession(
      action,
      email.current.value,
      password.current.value
    ).then(this.handleSuccess).catch(this.handleErrors);
  }

  resetForm() {
    if (!this.email.current || !this.password.current) { return }
    const { email, password } = Form.defaultProps;
    this.email.current.value = email;
    this.password.current.value = password;
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <h1>{this.props.title}</h1>
        <input
          name="name"
          type="email"
          ref={this.email}
        />
        <input
          name="password"
          type="password"
          autoComplete="none"
          ref={this.password}
        />
        <button type="submit">Submit</button>
      </form>
    )
  }
}

Form.propTypes = {
  title: PropTypes.string.isRequired,
  action: PropTypes.string.isRequired,
  onSuccess: PropTypes.func,
  onError: PropTypes.func
}

Form.defaultProps = {
  errors: '',
  email: '',
  password: ''
}

export default Form;
Enter fullscreen mode Exit fullscreen mode

Form breakdown

  • Both email, password creates a ref createRef() that we attach later to React elements via the ref attribute.
  • handleSuccess method executes resetForm method, and callback function from the giving props (if found any!).
  • handleErrors method executes the callback function from the giving props (if found any!) with reason.
  • handleSubmit method prevent the default form behavior, and executes the auth.userSession to create and account or login a user.

Login component

// src/components/Login.js

import React from 'react';
import { withRouter } from 'react-router-dom';
import Form from '../shared/Form';
import { Consumer } from './AppProvider';

const Login = props => <Consumer>
  {({ state, ...context }) => (
    <Form
      action="signIn"
      title="Login"
      onSuccess={() => props.history.push('/dashboard')}
      onError={({ message }) => context.setMessage(`Login failed: ${message}`)}
    />
  )}
</Consumer>;

export default withRouter(Login);
Enter fullscreen mode Exit fullscreen mode

Login breakdown

Login is a stateless component wrapped by Consumer that subscribes to context changes. If successfully logged in the user will be redirect to a protected page (dashboard) otherwise error message will be popped up.

Signup component

// src/components/Signup.js

import React from 'react';
import { withRouter } from 'react-router-dom';
import Form from '../shared/Form';
import { auth } from '../firebase';
import { Consumer } from './AppProvider';

const Signup = props => <Consumer>
  {({ state, ...context }) => (
    <Form
      action="createUser"
      title="Create account"
      onSuccess={() => auth.logout().then(() => {
        context.destroySession();
        context.clearMessage();
        props.history.push('/accountCreated');
      })}
      onError={({ message }) => context.setMessage(`Error occured: ${message}`)}
    />
  )}
</Consumer>;

export default withRouter(Signup);
Enter fullscreen mode Exit fullscreen mode

Signup breakdown

Signup is a stateless component wrapped by Consumer that subscribes to context changes. Firebase by default automatically logs the user in once account created successfully. I've changed this implementation by making the user log in manually after account creation. Once onSuccess callback fires we log the user out, and redirect to /accountCreated page with custom message and a call to action "Proceed to Dashboard" link to login. If account creation fails error message will be popped up.

App component (container)

// src/components/App.js

import React, {
  Component,
  Fragment
} from 'react';
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom';

import AppProvider, {
  Consumer
} from './AppProvider';
import Login from './Login';
import Signup from './Signup';

import Navbar from '../shared/Navbar';
import FlashMessage from '../shared/FlashMessage';

class App extends Component {
  render() {
    return (
      <AppProvider>
        <Router>
          <Fragment>
            <Navbar />
            <FlashMessage />
            <Route exact path="/" component={() => 
              <h1 className="content">Welcome, Home!</h1>} />
            <Route exact path="/login" component={() => <Login />} />
            <Route exact path="/signup" component={() => <Signup />} />
            <Router exact path="/dashboard" component={() => <Consumer>
              {
                ({ state }) => state.currentUser ?
                  <h1 className="content">Protected dashboard!</h1> :
                  <div className="content">
                    <h1>Access denied.</h1>
                    <p>You are not authorized to access this page.</p>
                  </div>
              }
            </Consumer>} />
            <Route exact path="/signedOut" component={() => 
              <h1 className="content">You're now signed out.</h1>} />
            <Route exact path="/accountCreated" component={() => 
              <h1 className="content">Account created. <Link to="/login">
              Proceed to Dashboard</Link></h1>} />
          </Fragment>
        </Router>
      </AppProvider>
    );
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

App (container) breakdown

Its pretty straightforward right here! The navigational components Routers wrapped by AppProvider to pass data through the component tree. The /dashboard route component has a protected content (page) that is served only for authenticated users, and no users are signed in we display the Access denied message instead of our private content/page.

Demo

Check out demo-gif here


Feedback are welcome If you have any suggestions or corrections to make, please do not hesitate to drop me a note/comment.

Discussion (3)

Collapse
cdock profile image
Conor Dockry

I would get rid of auth.js. Seems like extra indirection for not much gain.

You can refactor action in the form:

<Form
      action={firebase.auth().signInWithEmailAndPassword}
      title="Login"
      onSuccess={() => props.history.push('/dashboard')}
      onError={({ message }) => context.setMessage(`Login failed: ${message}`)}
    />

Enter fullscreen mode Exit fullscreen mode

Then in Form.js

handleSubmit(event) {
    event.preventDefault();
    const {
      email,
      password,
      props: { action }
    } = this;

   action(
      email.current.value,
      password.current.value
    ).then(this.handleSuccess).catch(this.handleErrors);
  }

Enter fullscreen mode Exit fullscreen mode

I recently made something similar using React.Context to manage auth state. On some advice from @kentcdodds he made a point how, if your entire app depends on an authorized user, it's much easier to handle it all in it's own module then just import user state in the files that need it, in the Components that are only rendered for an authorized user.

With firebase auth I'm just rendering a complete different root Component (App or Login) in my main index file like this:

const renderApp = () => {
  ReactDOM.render(<App />, root)
}
const renderLogin = () => {
  ReactDOM.render(<Login />, root)
}

firebase
  .firestore()
  .enablePersistence()
  .then(() => {
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        renderApp()
      } else {
        renderLogin()
      }
    })
  })

Enter fullscreen mode Exit fullscreen mode

But this could also be renderApp(user) and you could define routes appropriately in there. Easier to not have to check for user everywhere, and in each Component you can just:

import firebase from '../firebase'

....

const user = firebase.auth().currentUser 
Enter fullscreen mode Exit fullscreen mode

where needed, knowing the user is logged in at this point.

Collapse
timothyahong profile image
Timothy Ahong • Edited on

A couple of typos I found. Might be useful for other people who are working through the tutorial!

1)
In src/components/App.js the folder specified for Navbar and FlashMessage is "shared/" instead of "components/". In the first step these files are added to the "components/" folder. So src/components/App.js should have the following 2 lines changed.

import Navbar from '../components/Navbar';
import FlashMessage from '../components/FlashMessage';

2)
Since we have our App.js under "components/" we will need to change the default src/index.js that you get from create-react-app. The default index.js assumes App.js is located under the src folder and the path needs to be udpated to components/App.js. This was not mentioned in the tutorial.

Collapse
gijoegotyler profile image
xd puggle

I am sort of confused by this however it does work. I am trying to understand how to add another component that would act as a better dashboard.