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...
- Set up Authentication Providers with Firebase.
- Produce our login handling component and Build an
authenticate
method to request authentication from the relevant sign in provider - Build an 'authhandler` method to do stuff once we have the results from the authenticate method.
- Set up logic within the Inventory component to display relevant component to authorised users.
- Implement Firebase rules to effectively lock it down in the back-end.
So lets get started...
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
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.
- Go to your Firebase console and go to
Authentication
- Select Github inside
set up sign in method
- Now, it will want an
App ID
andApp Secret
. Also note that it provides aURL
to return back to after the authentication attempt. - Navigate to [https://github.com/settings/developers]
- Register a new OAuth application, and paste the Authorization callback URL from firebase
- The Client Id and Client Secret will be now available to add to Firebase.
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:
- Set up a Login Component
- Configure the parent Inventory Component with an authentication method
- Pass the authentication method to the login via props
- Configure the login component to use the authentication method
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
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:
- Call a method in props called authenticate. This doesnt exist but it will soon.
- 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}
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'
- 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 - We specify our auth provider
- 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
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.
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.
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.
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.
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 is 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.
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} />}
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.
If they pass the first two tests we know they are the owner and can return the component as normal.
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
So lets sort out the logout method, there is two tasks to do during logout:
- Sign out of the auth provider
- Clear the state of the current user details
This can be done in a line each:
https://gist.github.com/6e85d040afc6099bcc6a5f72f7899de9
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:
- Get Firebase to check if there is a user
- 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.
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:
- Go to Firebase
- Go to the
Database
section and thenRules
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:
- Read access is allowed for everyone anywhere.
- 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)
- 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
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.