5

I'm new to web development and started learning ReactJS.

Till now there is no problem with building simple web apps.

Now I wanted to use authentication using Firebase.

I know how to authenticate a user using Firebase but I'm not able to get how to design frontend to restrict access to certain pages.

Previously, I used PHP and in that I used session variable and include to include the parts of HTML to show to the user.

But I don't know how to do it in ReactJS using Firebase.

Initial and failed approach:

I thought of updating this.state.loggedIn and using that to show the component. But this is not a correct way because I'm sending both the components to display and easily we can change the state of a component using React developer extension in Chrome.

This is my main index.js code:

import React from 'react';
import ReactDOM from 'react-dom';
import Login from './components/Login/Login';
import Home from './components/Home/Home';
import fire from './components/Fire';


class App extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            loggedIn: false,
        };

        this.authListener = this.authListener.bind(this);

    }

    componentDidMount() {
        this.authListener();
    }

    authListener() {
        fire.auth().onAuthStateChanged(user => {
            if (user) {
              // User is signed in.
                console.log(user);
                this.setState({
                    loggedIn: true
                })
            } else {
              // No user is signed in.
                this.setState({
                    loggedIn: false
                });
            }
        });

    }

    render() {

        return (
            <div>
                {this.state.loggedIn ? <Home/> : <Login/>}
            </div>
        );
    }
}

ReactDOM.render(
    <App/>, document.getElementById('app'));

And In Login.js, I'm calling the Firebase authentication function.

Just the snippet:

fire
    .auth()
    .signInWithEmailAndPassword(this.state.email, this.state.password)
    .catch(function (error) {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          console.log(error);
          // ...
     });

The authentication is working fine and I can see it in the console log.

So, how should I do it in ReactJS + Firebase to authenticate?(Is it posible without using any other packages like react-routes etc?)

Or should I be using Cloud functions for this?

SkrewEverything
  • 2,393
  • 1
  • 19
  • 50
ssh
  • 491
  • 1
  • 8
  • 19
  • You could do view access restriction using react-cookies and checking creds when switching views and have conditional rendering. Not pretty but would work – BSchnitzel Jun 27 '18 at 14:05
  • So, you want to send the only required HTML/JS to the client like in PHP for security purposes. Am I right? – SkrewEverything Jun 28 '18 at 12:03
  • @SkrewEverything Yes. If the user is logged-in, I should send different component to the user than when he is logged-in. If the user is not logged-in, I should not send the **logged-in component** to the user at all. – ssh Jun 28 '18 at 22:14
  • Did you find the solution? – unknownymouse Jul 08 '18 at 20:13
  • @unknownymouse No. I'm still waiting on someone to answer it. – ssh Jul 08 '18 at 20:15
  • Maybe this tutorial helps some people coming across this question: [React + Firebase Authentication](https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/). – Robin Wieruch Dec 30 '18 at 06:47

3 Answers3

1

UPDATE:

Everything you link in your html files should always come from your public directory. So, keep your bundled .js files in your public directory.

I have wrote some time saving tips for beginners. You can find more about that beginners tips here.


It is actually simple.

If you want to send only required html file to the browser, you have to hit the server every time you need a whole different html.

You said that you used PHP. In PHP, we use sessions to know whether a person is logged-in or not.

So, we try to implement it here.

First lets look at how sessions in PHP work. Taken from an SO Answer

In the general situation :

  • the session id is sent to the user when his session is created.
  • it is stored in a cookie (called, by default, PHPSESSID)
  • that cookie is sent by the browser to the server with each request
  • the server (PHP) uses that cookie, containing the session_id, to know which file corresponds to that user.

The data in the sessions files is the content of $_SESSION, serialized (ie, represented as a string -- with a function such as serialize) ; and is un-serialized when the file is loaded by PHP, to populate the $_SESSION array.


Sometimes, the session id is not stored in a cookie, but sent in URLs, too -- but that's quite rare, nowadays.


For more informations, you can take a look at the Session Handling section of the manual, that gives some useful informations.

For instance, there is a page about Passing the Session ID, which explains how the session id is passed from page to page, using a cookie, or in URLs -- and which configuration options affect this.

So, session is just a cookie. Then I guess we can use a cookie to find out about the user login status.

In Firebase, there is a problem with cookies. You can only set cookie name as __session. Other names are ignored by the Firebase and as a result you can't read cookies on the server.

You need to use Firebase Functions to do some backend work and serve html based on the user login status.

So, setup the Firebase Functions in your project directory. In the root,

$ firebase init functions

Now you have to say to Firebase that any requests coming to your domain must invoke a function.

To do that you have to write rewrites in your firebase.json file. Our function name will be main.

{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "/dev",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites":[{
      "source": "**",
      "function": "main"
    }]
  }
}

Now create an index.js anywhere(Prefer root of the project directory as this is the main file).

index.js

import express from 'express';
import * as functions from 'firebase-functions';
import fs from 'fs';
var cookieParser = require('cookie-parser')

const app = express();
app.use(cookieParser());

app.get('**', (req, res) => {
    // Check the custom set cookie for user
    // If the user is not logged-in, redirect to Login. Otherwise, redirect to Home page
    if (req.cookies.__session == 'loggedin') {
        var index = fs.readFileSync(__dirname + '/src/Home/index.html', 'utf8');
    }
    else {
        var index = fs.readFileSync(__dirname + '/src/Login/index.html', 'utf8');
    }
    res.send(index);

});

export let main = functions.https.onRequest(app);

Some explanation about above parts:

  1. express: It is used to process the requests and send the response.

  2. firebase-functions: It is provided by the Google to use Firebase Functions.

  3. fs : We read appropriate html according to the user login status from the file system and serve it.

  4. cookie-parser: We use it to access our cookies easily.

Note: You need to convert it to ES5. So use babel command to convert it(I assume you saved index.js in your root.)

$ babel index.js -d functions

We are using cookie named __session to know about the user login status.

If the __session is set to loggedin, we send the user Home html. Otherwise, we send Login html.

Now, the real question is: "Where do we need to set the cookie __session?"

We set the cookie in the user browser.

We make use of onAuthStateChanged().

Call this method in your Login page component like:

componentDidMount() {
     this.authListener();
}


authListener() {
        fire.auth().onAuthStateChanged(user => {
            if (user) {
                // User is signed in.
                if (Cookies.get('__session') === undefined) {
                    Cookies.set('__session', 'loggedin', { expires: 3652 });
                    window.location.reload();
                }
            } else {
              // No user is signed in.
              Cookies.remove('__session');
            }
        });
    }

Note: Cookies object is imported from js-cookie. So, install it.

What we are doing here is:

  1. First the onAuthStateChanged() is called initially when the page is loaded.

  2. If the user is already logged-in, we enter into if block.

  3. Then we check for the __session cookie. This is important because sometimes user can clear all the cookies. When we try to read the cookie in the server, we get undefined and we send the login page to the user. Process is repeated from 1.

  4. Then we reload the page. When we reload the page, the user hits the server again. Then we can check for the __session cookie and send the Home html to the user.

SkrewEverything
  • 2,393
  • 1
  • 19
  • 50
  • I have a problem. How can I link my javascript file in the html? It says internal error. – ssh Jul 09 '18 at 00:43
  • Thanks. Your solution works as expected. I'm just gonna wait for some days if anyone comes with best answer before accepting an answer as someone kept bounty on my question. – ssh Jul 09 '18 at 01:29
0

Previously, I used PHP and in that I used session variable and include to include the parts of HTML to show to the user.

Firebase has a variety of ways it persists its auth state as described here: https://firebase.google.com/docs/auth/web/auth-state-persistence

By default it is local. On the Web, this is localStorage. This may be a drastic change for you since normally the auth state is in some sort of session or cookie that's done server side and client side.

So, how should I do it in ReactJS + Firebase to authenticate?(Is it posible without using any other packages like react-routes etc?)

But with Firebase, everything is done client side. Once a user has successfully authenticated, you will have access to their auth token as described here: https://firebase.google.com/docs/auth/users#auth_tokens

With this token, you would need to verify on the server side as described here: https://firebase.google.com/docs/auth/admin/verify-id-tokens in order to protect/secure any API access.

But for protecting certain pages in your React application, you will need to verify the auth state that is in localStorage in some sort of middleware. Preferably, you would have a listener for the auth state in one of your React lifecycle methods: https://firebase.google.com/docs/reference/js/firebase.auth.Auth#onAuthStateChanged

At any point in your application, the auth state becomes invalid, block them from viewing the page.

Cisco
  • 20,972
  • 5
  • 38
  • 60
  • I mean still we are sending all the components to the client. Is it still secure? – ssh Jun 28 '18 at 07:22
0

Well, I show you how I used firebase Auth on a React App. I just want you to know that it's not THE RIGHT WAY, you'll find a lot of ways of doing it.

So let's begin, first I'm using the Google Auth, I've a file called firebase.service.js where I handle the auth. The file looks like:

import firebase from 'firebase';

import { firebaseConfig } from './private';


export const firebaseApp = firebase.initializeApp(firebaseConfig);

export const auth = firebase.auth();
export const db = firebase.database();

export function loginGoogle(/* options */) {
  const provider = new firebase.auth.GoogleAuthProvider();

  return firebase.auth().signInWithPopup(provider);
}

The firebaseConfig object looks like this:

const firebaseConfig = {
    apiKey: API_KEY,
    authDomain: AUTH_DOMAIN,
    databaseURL: DB_URL,
    projectId: PROJECT_ID,
    storageBucket: STORE_BUCKET,
    messagingSenderId: MSG_SENDER,
};

You can find it at Firebase Console.

At the index file of the app, where it's mounted, I wrapped the ReactDOM.render method with an auth check, to be sure if an user is or isn't logged. The index looks like:

import React from 'react';
import ReactDOM from 'react-dom';
import firebase from 'firebase';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import { auth } from './services';

auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).then(() => {
  const stop = auth.onAuthStateChanged(() => {
    ReactDOM.render(<App />, document.getElementById('root'));
    stop();
  });
});

registerServiceWorker();

Now you're sure that if an user go away and come back to the app he won't need to login again if his session stills valid.

The next step is to create the PrivateRoute Component. Here is what I've done:

import * as React from 'react';
import { Redirect, Route } from 'react-router-dom';
import PropTypes from 'prop-types';

import { auth } from '../services';

function PrivateRoute({ component: Component, ...rest }) {
  const toRender = propsRender => (
    auth.currentUser ?
      <Component {...propsRender} /> :
      <Redirect
        to={{
          pathname: '/',
          state: { from: propsRender.location },
        }}
      />
  );

  return <Route {...rest} render={toRender} />;
}

PrivateRoute.propTypes = {
  component: PropTypes.func.isRequired,
  path: PropTypes.string.isRequired,
};

export default PrivateRoute;

Here you can see that the function checks if exists any logged user otherwise it sends the user back to the root path. Take note that I'm importing auth from the ./services not from the firebase, it's because this auth was initialized at firebase.service.js, then I use it through all application.

Now you can use this PrivateRoute as a common Route, the difference is that one will check if the user is logged. Take this as example:

import * as React from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import { Layout } from 'antd';
import moment from 'moment';

import 'moment/locale/pt-br';
import 'antd/dist/antd.css';

import { PrivateRoute, MainLogged, Login, Navbar, Page404 } from './components';

const { Header } = Layout;

moment.locale('pt-br');

function Main() {
  return <Redirect to="/login" />;
}

function App() {
  return (
    <BrowserRouter>
      <Layout>
        <Header style={{ paddingLeft: '0', backgroundColor: '#096dd9' }}>
          <Navbar />
        </Header>
        <Switch>
          <Route exact path="/" component={Main} />
          <Route exact path="/login" component={Login} />
          <PrivateRoute path="/app" component={MainLogged} />
          <Route component={Page404} />
        </Switch>
      </Layout>
    </BrowserRouter>
  );
}

export default App;

As you can see PrivateRouse is beeing used side by side the Route component from react-router.

I thought that it's all, if you want to ask anything else just comment, and if you want to take a look on the project from where I take this codes, you can find it here.

Gabriel Carneiro
  • 643
  • 5
  • 16
  • First I need to check `react-router-dom` and then I will comment on your answer – ssh Jun 28 '18 at 07:23
  • Still we are sending all the components to the client. Is it still secure? – ssh Jun 29 '18 at 08:55
  • Yep, when you build your app It'll be mimifed and uglifyed, it's impossible to understand the code. Just be sure to don't send the `.map` to the hosted server. If you're using react-create-app and react-script you'll have to delete the `.map` from the built app. – Gabriel Carneiro Jun 29 '18 at 11:07