DEV Community

Lukie Kang
Lukie Kang

Posted on

React Learnings in 2018 - Authentication

In our hypothetical React app we have:

  • Menu - where users can choose what to order
  • Order - where users can see what they have ordered
  • Inventory - which dictates what is available to order

In a real-life app, we might want to restrict the Inventory to an owner. This is what we are going to do in this post using Firebase Authentication to use Github, Twitter and Facebook login.

The process consists of the following parts.

  1. Set up Authentication Providers with Firebase
  2. Produce our login handling component and Build an authenticate method to request authentication from the relevent signin provider
  3. Build an 'authhandler` method to do stuff once we have the results from the authenticate method.
  4. Set up logic within the Inventory component to display relevent component
  5. Implement Firebase rules to effectively lock it down in the backend.

So lets get started...

Part 1 - Setting up our Authentication Providers

This section is all about setting things on the backend, in our case the back-end is actually Firebase and the services we want to authenticate with.

For each sign in provider there is two main steps:

  • Configure Firebase with API details from a sign-in provider
  • Obtain API keys and configure settings on the sign-in provider's developer site.

Facebook Authentication

  1. Go to your Firebase console and go to Authentication
  2. Select set up sign in method
  3. First set up Facebook, it will want an App ID and App Secret. Also note that it provides a URL to return back to after the authentication attempt. To get this we need to go to the Facebook Developers site

  4. Select My Apps and then Create a New App

  5. Once you have created the App you want to go to settings and basic to retrieve the App ID and App Secret. An App Secret is really important so be careful about where you store that info.

  6. YOu may need to add Facebook Login to your App, just next through the options. The option to add Facebook login should be easily found.

  7. Goto settings for Facebook Login and enable Browser OAuth login

  8. Copy the URL we referred to in step 3 and paste it into the field called Valid OAuth redirect URIs

  9. Go back to Firebase and click save on the Facebook setup prompt

Twitter Authentication

  1. Follow steps 1-3 as before but this time select Twitter.
  2. Go to apps.twitter.com and create a new application
  3. In the form add the callback URL you get from Firebase, fill out the ret as you see fit.
  4. You should then have access to the API key and API secret to paste into Firebase.

Github Authentication

This is fairly straightforward:

  1. Follow steps 1-3 as before but this time select Github.
  2. This time the url is [https://github.com/settings/developers]
  3. Register a new OAuth application, and paste the Authorization callback URL from firebase
  4. The Client Id and Client Secret will be now availiable to add to Firebase.

As you can see its a fairly similar process for most sign in providers once you navigate through thier various developer sites.

Part 2 - Login Component and Authentication Method

The first half of the proces is setting up the app to send a sign in request

Now we have Firebase ready to use a sign-in provider we need to provide the code to use it on a login page of some sort.

The steps to do this are:

  1. Set up a test Login Component
  2. Configure the parent Inventory Component and create an authentication method
  3. Pass the authentication method to the login via props
  4. Configure the login component to use the authentication method.

In the second part we will look at what we do once we have attempted authentication.

Setting up and Testing the Login Component

The login component will exist to render some buttons to allow sign in to various providers. As it will do little else we can just make it a stateless functional component.

Lets just keep it simple to test it works:

js
import React from 'react'
const Login = props => (
<button>Github Login</button>
);
export default Login;

In our Inventory component let's see if we can get the login to render by first importing our Login component:

import Login from "./Login"

And then insert another return at the start of the render method, effectively redirecting the Inventory component to display the login component only for now:

return <Login />

Once we are happy nothing dumb will stop this from working lets delve into the methods we need on the button.

Making the button do something...almost

When we want to click on the button, we want it to begin the authentication process for the signin provider we want. For the rest of this post, ill focus on getting Github signin working and trust you can logic out doing the same for other sign-in providers.

So let's change our button in the login component to:

  1. Call a method in props called authenticate. This doesnt exist but it will soon.
  2. Assuming we will have more than one type of sign in button... as a parameter for this method we will take the name of the provider with a capital letter. Why a capital? I'll explain that too in a bit, I promise.

Now our Github sign-in button in the Login component looks like this:

<button onClick={() => props.authenticate('Github')}> Sign in with GitHub </button>

Note in a functional component, we reference props by passing the parameter through as opposed to using this

Also, don't forget to add the PropTypes, something like:

Login.propTypes = { authenticate: Proptypes.func.isRequired}

Building the Authenticate method

As authentication doesnt involve state at the top level we can write the authentication method at the Inventory component level which otherwise is deciding if the login component needs to be shown.

First, make sure to import Firebase else little will happen, Firebase has the methods we need to make this easy:

import firebase from 'firebase';

We also need to refer our base compononent (SEE THE DATA PERSISTANCE POST)

import {firebaseApp} from '../base'

  1. For the method we pass in the value from the button as Provider (i.e "Github"). This is a clever way of checking the signup provider
  2. We specify our auth provider
  3. We use firebase's methods to produce a popup that uses the auth provider specified to prompt for iign in

The code looks something like this:

js
authenticate = provider => {
const authProvider = new firebase.auth[
${provider}AuthProvider]();
firebaseApp.auth().signInWithPopup(authProvider).then(this.authHandler)
}

Part 3 - Handling the authentication response

The last line of that code snippet calls authHandler which is all about what our app does once the authentication data is returned.

js
authHandler = async authData => {
console.log(authData);
}

If all went went so far. You now should be able to sign into github and see an object returned to the console.

On our use case, authhandler needs to do three things:

  • Look up the current store in the firebase DB
  • Claim it as current user if there is no owner
  • Set the state of the inventory component to reflect the current user so it now knows who is logged in.

Look up the current store in the firebase database

We need to look up the current store and see if there is an owner, to do that we need to look at our database so import base from our base component:

import base, {firebaseApp} from '.../base'

Then we need to fetch details about the current store. But wait, we first need the Inventory component to know the storeid which we can get from the parent App component (which itself is getting it from React Router):

storeId ={this.props.match.query.storeId}

So back in our authHandler method in the Inventory component:

const store = await base.fetch( *STORE GOES HERE* )

To get the store name we need to pass the prop from App:

storeId={this.props.match.params.storeId}

So the finished command in authhandler looks like this:

const store = await base.fetch(this.props.storeId, {context: this})

If we don't use await here, store will be the promise as opposed to the result of the promise which is what we want.

2. Claim ownership if no current owner

Next we need to check if there is an owner, if not we save the owner information to the firebase DB.

js
if(!store.owner){
await base.post(
${this.props.storeId}/owner, {data: authData.user.uid})
}

At this point we should be able to check in the Firebase DB if an owner is being set.

3. Set the state of the inventory component to reflect the current user

To work out what to do when a user logs in we need to know two things:

  • What is the UID of the newly logged on user?

  • Who is the owner of the resource? If any?

The setstate command looks like this:

js
this.setState({
uid: authData.user.uid,
owner: store.owner || authData.user.uid
})

As we dont need this information elsewhere we can set State locally to the Inventory Component as opposed to the parent root app. This requires setting up state on the component:

js
state = {
uid: null,
owner: null
}

The entire authHandler method looks like this:

js
authHandler = async authData {
if(!store.owner){
await base.post(
${this.props.storeId}/owner, data: authData.user.uid)
}
const store = await base.fetch(this.props.storeId, {context: this})
this.setState({
uid: authData.user.uid,
owner: store.owner || authData.user.uid
})
}

If everything has gone well you should be able to log on and see the new keys in Inventory's state and an owner value in the Firebase DB store.

Part 4 - Displaying the right content

The inventory render method will need some logic to check the following scenarios:

  • IF they are NOT logged in THEN Show login component
  • IF Logged in and NOT the owner THEN Show "Do not have access message"
  • IF Logged in AND the owner THEN Show the inventory Component

Aside from this, we also want to make a logout button to allow a different login if need be.

Lastly we don't want to logon each time we refresh the page so lets recheck the current user automatically.

If NOT Logged In

The uid contained within State determines if they are logged on so we just need to return the login component if there isnt a uid availible:

if (!state.uid) {return <Login authenticate={this.authenticate} />}

Logged in but NOT the owner

Easy enough, we need to compare the uid in state with the owner in state:

if (this.state.uid !== this.state.owner){return <div>You are not the owner</div>}

You will probably want to spruce that up with a better response than a plain div but it will do for now.

Logged in as the owner

If they pass the first two tests we know they are the owner and can return the component as normal.

The Logout button

Ideally this should be a component but for the sake of brevity we wil define a JSX variable inside the render method:

const logout = <button onClick={this.logout}>Log Out</button>

We can then place the button on the 'not owner' and 'owner' return paths

The Logout Method

The button calls a method which doesnt exist so we should sort that out, there is two tasks to do during logout:

  1. Sign out of the auth provider
  2. Clear the state of the current user details

This can be done in a line each:

js
logout = async () => {
await firebase.auth().signOut();
this.setState({uid:null})
}

Rechecking we are logged in

When we refresh the page it would be good if it can check to see if we are logged in to avoid the login prompt. We just need to use the componentDidMount lifecycle method to:

  1. Get Firebase to check if there is a user
  2. Pass that user to our authHandler method

js
componentDidMount(){
firebase.auth().onAuthStateChanged(user => {
if(user) {
this.authHandler({user});
}
})
}

Part 5 - Securing the Firebase Backend

All that we have done so far is secured the client side, a determined person can still access and change the information in Firebase as we have left it open for anyone to read anad write.

Luckily for us, this is fairly straightforward to do:

  1. Go to Firebase
  2. Go to the Database section and then Rules

This should get you to the rules section of the database where previously we allowed both read and write to be true:

json
"rules" : {
"read": "true",
"Write": "true"
}

Instead we need to change this as follows:

json
"rules": {
".write": "!data.exists()",
".read":true,
"$room": {
".write": "auth != null && (!data.exists() || data.child('owner').val() === auth.uid) ",
".read": true
}
}

Lets explain that:

  1. Read access is allowed for everyone anywhere.
  2. A user can only write at the top layer (i.e where a store goes) if there is no current store (ie No data exists)
  3. Within a store ($room) write is only allowed if:
  • auth isnt null (the user is logged on)
  • Either, no data exists or the existing owner matches the current one

Conclusion

I have written about authetiacation with Passport JS before. This, overall, seems a little more friendlier as firebase handles some of the heavy lifting. Plus using async and await avoids some of the callback hell I experienced before.

Hopefully the above is enough to get the authentication ball rolling when it is needed. However I think some time with the Firebase docs when trying to use it would be a good idea.

Breaking it down into small steps is definately the way to avoid losing your mind when it comes to authentication.

Top comments (0)