Skip to content

Instantly share code, notes, and snippets.

@neosaurrrus
Created December 5, 2018 07:46
Show Gist options
  • Save neosaurrrus/31b56170ac791b1e666d58cddf5fdb3c to your computer and use it in GitHub Desktop.
Save neosaurrrus/31b56170ac791b1e666d58cddf5fdb3c to your computer and use it in GitHub Desktop.

This is part 13 of my React Learning series. Using knowledge gleaned from Wes Bos' React for Beginners among other things.

After our slight diversion to Proptypes let's get stuck into the meatiest of meaty things, Authentication and Authorisation. Let's explain what I mean by that:

Authentication - Checking to see who someone is

Authorisation - Controlling what can be accessed based upon who they are

In order to talk about it, we need an example app where we use it. So let's imagine an app where we have three core components:

  • 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

These are nested within an App component with a unique store ID. Allowing users to access their own instance of the app.

In this app, we 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.

Big topics get confusing fast, so we are going to break it down into smaller steps...

  1. Set up Authentication Providers with Firebase.
  2. Produce our login handling component and Build an authenticate method to request authentication from the relevant sign in 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 relevant component to authorised users.
  5. Implement Firebase rules to effectively lock it down in the back-end.

So lets get started...

Part 1 - Setting up our Authentication Providers

This section is all about setting things up on the back-end, in our case the back-end is actually Firebase and the services we want to authenticate with. We wont be dealing with creating our own authentication as that gets messy fast!

For each sign in provider there is two basic steps:

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

Note on getting API Keys

Now, I actually wrote sections on getting API keys for Facebook, GitHub and Twitter authentication. Since time of writing (2018), they have changed the way it works. More importantly, they will probably change it again by the time you read this, so its best you figure this out from their own documentation rather than follow old advice. I'll at least include the GitHub one as its a fairly simple example of what we are trying to do.

GitHub API Key

  1. Go to your Firebase console and go to Authentication
  2. Select Github inside set up sign in method
  3. Now, it will want an App ID and App Secret. Also note that it provides a URL to return back to after the authentication attempt.
  4. Navigate to [https://github.com/settings/developers]
  5. Register a new OAuth application, and paste the Authorization callback URL from firebase
  6. The Client Id and Client Secret will be now available to add to Firebase.

Part 2 - Login Component and Authentication Method

So let's look at setting up the app to send a sign in request. AKA Authentication. AKA "Who are you?"

We need to provide the code to talk to Firebase. This is traditionally on a login page of some sort.

The basic steps to do this are:

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

Setting up and Testing the Login Component

The login component will render some buttons to allow sign in to the providers we set up in Step 1. As it will do little else we could just make it a stateless functional component.

https://gist.github.com/d8bf7748f7434cd3ab383843f267e952

In our Inventory component, we need to test we can see the button. First import 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 button from working lets delve into the methods we need when the button is used

Making the button do something...almost

When we 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 sign in working and I'll trust you can figure out the same for other sign-in providers.

So our button needs 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 doesn't need to involve state at the top level we can write the authentication method at the Inventory component level which can decide 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 component (look back at my data persistence post for more info on that) which contains the API details:

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 sign-in

The code looks something like this:

https://gist.github.com/9c27589ba6b7b4fb0f7c62c02182481c

Part 3 - Handling the Authentication response

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

https://gist.github.com/674163388dd095da569a9e394e85f648

If all went well so far. You now should be able to sign into GitHub and see an object returned to the console. This object contains all the information we need.

Now this part starts getting very use case specific, so I might lose you here but the main point is that the authData object gives us information we can check against other information to determine what should happen. That's authentication in a nutshell.

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

  • Look up the current store in the firebase DB
  • Set the owner as the 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.

1. Look up the current store in the Firebase database

In our app 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 to get the store from Firebase:

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.

https://gist.github.com/c6e67bfb08a3af1f7491851cc739c602

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:

https://gist.github.com/93e8840ed4774ae1e2a801c37787dce7

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

https://gist.github.com/0369fb29eab8a7ab6742be730d5c52b3

The entire authHandler method looks like this:

https://gist.github.com/34d094b5f00563a314ef64088a0de5b9

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:

  1. IF they are NOT logged in THEN Show login component
  2. IF Logged in and NOT the owner THEN Show "Do not have access message"
  3. IF Logged in AND is the owner THEN Show the inventory Component
  4. Aside from this, we also want to make a logout button to allow a different login if need be.
  5. Lastly we don't want to logon each time we refresh the page so lets recheck the current user automatically.

1. If NOT Logged In THEN show login component

The UID contained within State determines if they are logged on so we just need to return the login component if there isn't a uid available:

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

2. Logged in but NOT the owner THEN show "You do not have access" type thing

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.

3. Logged in AND is the owner THEN show the Inventory Component.

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

4. The Logout button

Almost there, 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

So lets sort out the logout method, 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:

https://gist.github.com/6e85d040afc6099bcc6a5f72f7899de9

5. 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

https://gist.github.com/e33a91f614ab1920a004b0abe3d530ca

Phew, that is a lot to think about but hopefully you can see that by breaking it down into little steps it is slightly less maddening.

Part 5 - Securing the Firebase Back-end

All that we have done so far has 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 and write (if you followed my previous post about Firebase)

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:

https://gist.github.com/0f48b2037da4c1d1c4f0a73d5a3942f0

Instead we need to change this as follows:

https://gist.github.com/e2771cb003845d2cb021ba5d4068272e

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 authentication 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 very good idea.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment