React Firebase Authentication

mahmoudelmahdi

Mahmoud Elmahdi

Posted on June 18, 2018

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.

💖 💪 🙅 🚩
mahmoudelmahdi
Mahmoud Elmahdi

Posted on June 18, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React Firebase Authentication
react React Firebase Authentication

June 18, 2018