React Firebase Authentication

June 20, 2018 0 Comments

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

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

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

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
}

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
}

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) => authemail, password</span><span class="p">${</span><span class="nx">action</span><span class="p">}</span><span class="s2">WithEmailAndPassword; /
* * Destroy current user session /
const logout = () => auth.signOut(); export { userSession, logout
}

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
}

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;

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);

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;

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;

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: </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">)} />
)}
</Consumer>;

export default withRouter(Login);

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: </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">)} />
)}
</Consumer>;

export default withRouter(Signup);

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;

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.


Tag cloud