Add Google Authentication using Firebase in React+Redux Application

Single page applications(SPA) are quite famous these days, they are easy to build thanks to all the available libraries and framework. Angular (by Google) and React(by Facebook) are the most famous options available to explore these days. Because of these, front-end applications are now easy to manage and maintain. But even if we are able to create this SPA using all the technologies, we still need some server side logic to persist our data, and most importantly we need authentication so that each user can perform an action in the scope they are authorized to do.

Below is the diagram which shows the authentication flow.

oauth_implicit

Quick Intro of Firebase

Formerly known as Google Cloud Messaging (GCM), Firebase Cloud Messaging (FCM) is a cross-platform solution for messages and notifications for Android, iOS, and web applications, which currently can be used at no cost.

Today we will try to build a simple SPA using React, Redux, and Firebase(it will provide the google authentication)

Prerequisite-

Installed Software

We will follow the https://github.com/jainamit333/react_google_authetication to walk through the development.

Branch Name:- vanilla

Create a directory name react-authentication

 mkdir google-authentication-react
 cd google-authentication-react

 

Create Directory Structure as follows

Screen Shot 2017-08-05 at 2.00.09 PM

Add following code to package.json 

{
"name": "google-authentication-react",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "npm run build; node server/index.js",
"start-dev": "nodemon server/index.js",
"build": "webpack -p",
"build-dev": "webpack -w",
"build-sass": "node-sass -w ./client/styles/main.scss -o ./client/styles/mainSheet",
"test": "echo \"Error: no test specified\" && exit 1",
"stats": "webpack --env production --profile --json > stats.json"
},
"dependencies": {
"axios": "^0.16.1",
"babel": "^6.5.2",
"babel-core": "^6.18.0",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-2": "^6.24.1",
"body-parser": "^1.17.1",
"bootstrap": "^4.0.0-alpha.6",
"css-loader": "^0.28.0",
"express": "^4.15.2",
"firebase": "^4.2.0",
"muicss": "^0.9.20",
"node-sass": "^4.5.2",
"react": "^15.6.1",
"react-addons-css-transition-group": "^15.6.0",
"react-addons-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-redux": "^5.0.4",
"react-router": "^3.0.0",
"react-router-dom": "^4.1.2",
"reactstrap": "^4.8.0",
"redux": "^3.6.0",
"redux-logger": "^3.0.1",
"redux-thunk": "^2.2.0",
"sass-loader": "^6.0.3",
"style-loader": "^0.16.1",
"volleyball": "^1.4.1",
"webpack": "^2.7.0",
"webpack-livereload-plugin": "^0.10.0"
},
"devDependencies": {
"chai": "^3.5.0",
"cross-env": "^3.1.4",
"expose-loader": "^0.7.3",
"mocha": "^3.1.2",
"nodemon": "^1.11.0",
"react-hot-loader": "^1.3.1",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.1",
"webpack-dashboard": "^0.4.0",
"webpack-dev-server": "^2.6.1"
}
}

At line number 26 we have added the dependency for firebase.

Note:- This package.json may contain many extra dependencies as I have extracted it from my other project just for tutorial purpose.

Install all the added dependencies

npm install

Add web pack Config code


const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const webpack = require('webpack');

module.exports = {
entry: './client/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'client/dist')
},
context: __dirname,
resolve: {
extensions: ['.js', '.jsx', '.json', '*']
},
devtool:'cheap-module-source-map',
devServer: {
inline: true,
contentBase: './dist',
port: 3001
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
options: {
presets: ['react', 'es2015','stage-2']
}
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}]
},
plugins: [
new webpack.DefinePlugin({

'process.env.COSMIC_BUCKET': JSON.stringify(process.env.COSMIC_BUCKET),
'process.env.COSMIC_READ_KEY': JSON.stringify(process.env.COSMIC_READ_KEY),
'process.env.COSMIC_WRITE_KEY': JSON.stringify(process.env.COSMIC_WRITE_KEY)
}),
new LiveReloadPlugin({appendScriptTag: true})

]
};

 

React and Redux related files

  • server/index.js
<pre>const express = require('express');
const app = express();
const path = require('path');
const volleyball = require('volleyball');
app.use(volleyball);
//serve up static files
app.use(express.static(path.resolve(__dirname, '..', 'client')));
app.use(express.static(path.resolve(__dirname, '..', 'node_modules')));
app.use(function (err, req, res, next) {
    console.error(err);
    console.error(err.stack);
    res.status(err.status || 500).send(err.message || 'Internal server error.');
});

// handle every other route with index.html, which will contain
// a script tag to our application's JavaScript file(s).
app.get('*', function (request, response) {
    response.sendFile(path.resolve(__dirname, '..', 'client', 'index.html'))
});
//listen on port 3000
app.listen(process.env.PORT || 3000, function () {
    console.log("Rockin' out on port 3000 homie");
});</pre>

We are starting the project on port 3000.

  • redux/actions/actions.js
var constants = {
START_AUTHENTICATING:'START_AUTHENTICATING',
AUTHENTICATION_SUCCESSFUL:'AUTHENTICATION_SUCCESSFUL',
AUTHENTICATION_FAILED:'AUTHENTICATION_FAILED',
ALREADY_LOGIN:'ALREADY_LOGIN',
LOGOUT:'LOGOUT',
LOGOUT_SUCCESSFUL:'LOGOUT_SUCCESSFUL',
LOGOUT_ERROR:'LOGOUT_ERROR',
}

export default constants;

We have created constants for actions which will be supported for out dummy application

  • redux/actions/auth.js
import constant from './actions'

export const startAuth = keyWord => {

return {
type: constant.START_AUTHENTICATING,
authenticated:false,
authenticating:true
}
}

export const alreadyLogin = response => {

return{
type:constant.ALREADY_LOGIN,
authenticated:true,
authenticating:false,
user:response

}
}

export const authError = error => {

return {
type: constant.AUTHENTICATION_FAILED,
authenticated:false,
authenticating:false,
error
}
}

export const authSuccess = response => {
return {
type: constant.AUTHENTICATION_SUCCESSFUL,
user:response,
authenticated:true,
authenticating:false
}
}

These are the various activities that will be spawn during our login lifecycle.
We usually create a different actions file for a different piece of flow.

  • redux/reducers/auth.js
import constants from '../actions/actions'

const auth = (state = [], action) => {
switch (action.type) {

case constants.START_AUTHENTICATING:
return Object.assign({}, state, {
authenticated: false,
authenticating: true,
user: action.user
}, ...state)
break;
case constants.AUTHENTICATION_FAILED:
return Object.assign({}, state, {
authenticated: false,
authenticating: false,
user: {}
}, ...state)
break;
case constants.AUTHENTICATION_SUCCESSFUL:
return Object.assign({}, state, {
authenticated: true,
authenticating: false,
user: action.user
}, ...state)
break;

case constants.ALREADY_LOGIN:
return Object.assign({}, state, {
authenticated: true,
authenticating: false,
user: action.user
}, ...state)
break;
default:
return state
}

}
export default auth

As you can see, on the success we are setting user and setting param authenticated and authenticating accordingly in every case.
Other two param are mostly if we want to add loading bar during the login process.

 

  • redux/reducer.js
import {combineReducers} from "redux";
import auth from "./reducers/auth";

const googleBooks = combineReducers({
auth
})

export default googleBooks

This is the place where we combine all our reducer.
For now, we have only one main reducer which we are adding in line 5.

redux/store.js

import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';
import {createLogger} from 'redux-logger';

const initialState = {
auth:{
authenticated:false,
authenticating:false,
user:{},
}
}

const store = createStore(
reducer,
initialState,
applyMiddleware(
createLogger(),
thunk
)

);
export default store;

We are creating a redux store and adding our combined reducer in line 15.

  • client/index.html
</pre>
<pre><!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Books Around You</title>
    			<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"
            integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
            crossorigin="anonymous"></script>

    <script src="/dist/bundle.js" defer></script>
</head>
<body>


<div id="root"></div>


</body>
</html></pre>
<pre>

Creating a placeholder for all our react component

  • client/index.js
</pre>
<pre>import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import store from '../redux/store';
import Dashboard from "./components/Dashboard";

ReactDOM.render(
    <Provider store={store}>
        <Dashboard />
    </Provider>,
    document.getElementById('root')
);</pre>
<pre>

we are adding dashboard component as out main component which will be added to root div

  • client/component/Dashboard.js
</pre>
<pre>import React, {Component} from "react";
import {connect} from "react-redux";
import Navigation from "./Navigation";
import {login} from "../../services/firebase/auth";
import UserInfoPanel from "./UserInfoPanel";

class Dashboard extends Component {

    componentDidMount() {
        login()
    }

    render() {
        return (


<div>
                <Navigation />


<div className="mui-row">
</div>



<div className="mui-row">
                    <UserInfoPanel/>
</div>


</div>


        )
    }
}

function mapStateToProp(state) {
    return state;
}

export default connect(mapStateToProp)(Dashboard)</pre>
<pre>

We are calling login method on component did mount.
So it will check if we are already login or not before mounting this component

  • client/component/Navigation.js
</pre>
<pre>import React from "react";
import {connect} from "react-redux";
import {login, logout} from "../../services/firebase/auth";

class Navigation extends React.Component {

    render() {

        var styles = {

            marginTop:{
                marginTop:'10px'
            },
            baseColor:{
                color:'#a83808'
            }

        }
        return (
            <nav className="navbar navbar-default">


<div className="container-fluid">


<div className="navbar-header">
                        <a className="navbar-brand" href="#" >
                            <span  className="glyphicon glyphicon-bishop " aria-hidden="true" style={styles.baseColor}></span>
                        </a>
</div>



<ul className="nav navbar-nav navbar-right ">


	<li>
                            {this.props.authenticated && <span style={styles.marginTop} onClick={logout} className="btn btn-sm btn-danger">Logout</span>}
                            {!this.props.authenticated && <span style={styles.marginTop} onClick={login(this.props.dispatch)} className="btn btn-sm btn-danger">Login</span>}
</li>


</ul>


</div>


            </nav>
        );
    }
}

function mapStateToProps(state) {
    return {
        authenticated: state.auth.authenticated,
    }
}

export default connect(mapStateToProps)(Navigation)</pre>
<pre>

If the user is login LOGOUT button will render another wise LOGIN button.

  • client/component/UserInfoPanel.js
</pre>
<pre>import React from 'react';
import {connect} from "react-redux";

class UserInfoPanel extends React.Component {

    render() {
        const styles = {
            card:{
                width: '20em',
                position: 'relative',
                display: 'flex',
                flexDirection:'column',
                backgroundColor: '#fff',
                border: '1px solid rgba(0,0,0,.125)',
                borderRadius: '.25rem',
                padding:'3px',
                margin:'2px'
            }
        }
        return (


<div className="card" style={styles.card}>
                { this.props.authenticated && <img className="card-img-top img-thumbnail" src = {this.props.user.photoURL} alt="Card image cap"/> }
                { this.props.authenticated &&


<div className="card-block">


<h4 className="card-title">{this.props.user.displayName}</h4>


{this.props.user.email}

</div>


                }
</div>


        );
    }
}

function mapStateToProps(state) {
    return {

        authenticated: state.auth.authenticated,
        authenticating: state.auth.authenticating,
        user: state.auth.user
    }
}

export default connect(mapStateToProps)(UserInfoPanel)</pre>
<pre>

This component is to show the information of the login user.
In highlighted part, you can check we are mapping state param to props of the component.
This is a connected component.

Firebase related Files

  • services/firebase/config.js
import firebase from 'firebase'

const config = {

apiKey: "<your api key from google developer console.>",
authDomain: "<auth domain from firebase project eg >",
databaseURL: "<databse url from firebase>",
storageBucket: "<storage bucket from firebase>",

}

firebase.initializeApp(config);
export const provider = new firebase.auth.GoogleAuthProvider();
provider.addScope('https://www.googleapis.com/auth/plus.login')
export const firebaseAuth = firebase.auth

Replace the config part from your configs.
On line 12 we are initializing firebase.
On line, we are creating a google authentication provider.
Line no 14 states the scope of google api we are using, for now, we only need plus.login,
as we are using only from authentication.
When you need some other access also add other scopes to the same provider in next line.

  • services/firebase/auth.js
import { firebaseAuth, provider} from './config'
import {alreadyLogin, authError, authSuccess, startAuth} from "../../redux/actions/auth";
import store from '../../redux/store'

export function logout () {

return firebaseAuth().signOut()
}

function doLogin() {

firebaseAuth().signInWithPopup(provider).then(function(result) {
store.dispatch(authSuccess(result.user))

}).catch(function(error) {
store.dispatch(authError(error))
});
}

export function login () {

firebaseAuth().onAuthStateChanged((response) => {
if(response){
store.dispatch(alreadyLogin(response))
}else{
store.dispatch(startAuth())
doLogin()
}
});
return
}

In login method we are first checking of user is already login or not.
If not it will called do login method otherwise, it will called the action login already.

In doLogin method we are login usingn google provider.
If it is successful we are calling authSuccess action otherwise
authError action.

NOTE:- Since we are using Google authentication from firebase, it has to be enabled in firebase.Inside your firebase project go to authentication, go to sign-in-method and enable google provied.

Advertisements