React: User Authentication With Google OAuth 👮♀️
Intro
We have previously covered the process of a manual user authentication process using Passport JS and Express.
Now let's cover the process of letting someone else handle our user authentication. This is an introduction to user authentication in React using Google OAuth.
Here are some quick notes on the differences between a standard email/password authentication and OAuth Authentication
Email/Password Authentication
- We store a record in a database with the user's email and password
- When the user tries to login, we compare email/password with what's stored in the DB
- A user is logged in when the enter the correct email/password
OAuth Authentication
- User authenticates with outside service provider (Google, LinkedIn, Facebook, Apple)
- User authorizes our app to access their information
- Outside provider tells us about the user
- We are trusting the outside provider to correctly handle identification of a user
- OAuth can be used for
- User Identification in our app
- Our app making actions on behalf of the user
OAuth Server vs Browser
There are two basic types of OAuth, Server and Browser.
OAuth for Servers
- Results in a token that a server can use to make requests on behalf of the user
- Usually used when we have an app that needs to access user data when they are not logged in
- Difficult to setup because we need to store a lot of info about the user
OAuth for JS Browser Apps
- Results in a token that a browser app can use to make requests on behalf of the user
- Usually used when we have an app that only needs to access user data while they are logged in
- Very easy to set up thanks to Googles JS lib to automate flow.
Steps for Setting Up OAuth
- Create a new project at console.developers.google.com/
- Set up an OAuth confirmation screen
- Generate an OAuth Client ID
- Install Google's API library, initialize it with the OAuth Client ID
- Make sure the library gets called any time the user clicks on the Login with Google button
Specific instructions for steps 1-3 will change frequently as Google is constantly changing their console. Just head over to the developer console, get a project going, get into the credentials area and create a new set of credentials. You can set your domain to http://localhost:3000 when they ask you for the white list.
When the process is done you will get a popup with a Client ID and a Client Secret. Don't worry about the secret for now, that is for OAuth Server setups.
Installing Google API Library
Google does not offer the API library as a node package, it must be added as a script to the head of your html index.
<script src="https://apis.google.com/js/api.js"></script>
You will know if you have retrieved the library successfully by going into your console and typing gapi
. If it's correct you will have an object available.
Scaffolding GoogleAuth Component
Our Google Authentication button is going to live in it's own component called GoogleAuth. Let's go ahead and get that scaffolded out real quick. We are going to be building this out inside of our Glitch Twitch clone project. We can create a simple class component with a React-Router Link.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
and then we want our Google Login button to be living in our Header in this case, so lets go ahead and just import that there.
import React from "react"
import { Link } from "react-router-dom"
import Logo from "../images/glitch-logo.png"
import GoogleAuth from "./GoogleAuth"
const Header = () => {
return (
<div className="ui secondary pointing menu">
<Link to="/" className="item">
<img src={Logo} className="ui mini" alt="glitch logo" />
</Link>
<div className="right menu">
<Link to="/" className="item">
All Streams
</Link>
<GoogleAuth /> // highlight-line
</div>
</div>
)
}
export default Header
Understanding GAPI
Regarding the Google API script that we have included in our index. This script is used by so many websites that Google has gone to great effort to keep the size of the script small. As such it only contains one function called load
. This function is then used to make an additional request for a more specific set of functions. In this case we want the OAuth2 functions.
gapi.load("client:auth2")
Once you make a request for these additional functions, running gapi
will show you your additional functionality.
After we load up this additional library we can register/initialize it with our OAuth Client ID.
gapi.client.init({ clientId: "clientid" })
So what we need to do is follow these steps inside of our GoogleAuth component, and we want to do it one time, when the component is first rendered.
Initializing GAPI in Component
We can start by requesting the auth2 library after the component mounts. If we were using functional components we would do this with the useEffect Hook.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
componentDidMount() {
window.gapi.load("client:auth2")
}
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
We prepend this window.
to make it clear that this is a variable that is available on window scope inside of our browser. If you do not add this React will tell you it is undefined because it is searching in the wrong scope.
Next we need to load up the client portion of the library. It takes some amount of time to make the request to Google and download this additional JavaScript library, so we need to get a callback of when that process is complete. To do that we add the callback as the second argument of the gapi.load
function.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
componentDidMount() {
window.gapi.load("client:auth2", () => {
window.gapi.client.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
})
}
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
And of course we use an environment variable so that we don't make our API key publicly available when we push this to our public repo. If you don't do that you will have to delete and remake the client ID, which is something I absolutely did NOT do 🙄. Environment variables in React must also be prepended with REACT_APP_
just like in Gatsby they must be prepended with GATSBY_
. If you are experiencing issues with this double check the documentation on environment variables in React. Create-React-App comes with the dotenv package pre-installed.
If parts of this are feeling unclear you can reference the official Google documentation Google Sign-In JavaScript client reference
The official documentation actually looks a little bit difference because it does not create a key:value pair for the auth2 method, so if we strictly followed the docs it would look like this, which is equivalent.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
})
}
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
I feel this is slightly less confusing.
This init function returns the GoogleAuth
object, which has several methods such as GoogleAuth.isSignedIn.get()
which returns a boolean. The full list of methods is available in the Google Docs.
Reference to Auth Instance
To test this out a little bit in the console we can first create a reference to an Auth instance.
const auth = gapi.auth2.getAuthInstance();
and then if we call that reference we can see the methods that we now have access too.
These methods should reflect the methods that you see in the documentation. Let us use these methods to sign in manually using the console.
auth.signIn()
and we immediately get the pop-up with the sign-in window
and we can then use the auth methods to check if we are signed in.
auth.isSignedIn.get()
and we get the boolean response back that we are indeed signed in.
Now that we understand how to do these things manually in the console, it will be easy to trigger these functions using our React Components.
Reference Auth Instance in Component
Because the .init
method is an Async Function it returns a Promise. We can therefore chain an action on the return of this promise with .then
.
We can use that action to repeat the process we did just above in the console, by setting the auth instance to a variable this.auth
.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance()
})
})
}
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
Set Variable & Force Re-render
Then we can force the component to re-render by updating state with setState
, and we will now have a boolean value for isSignedIn
which we can use to change the content in our components.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
state = { isSignedIn: null } // highlight-line
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance()
// update state so that component will re-render
this.setState({ isSignedIn: this.auth.isSignedIn.get() }) // highlight-line
})
})
}
render() {
return (
<Link to="/" className="item">
Google Auth
</Link>
)
}
}
export default GoogleAuth
and we can then create a little helper function that will check the status of our user and inject the relevant string into our component.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
state = { isSignedIn: null }
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance()
// update state so that component will re-render
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
})
})
}
// helper function
retrieveUserStatus() {
if (this.state.isSignedIn === null) {
return "UNKNOWN"
} else if (this.state.isSignedIn) {
return "SIGNED IN"
} else {
return "NOT SIGNED IN"
}
}
render() {
return (
<Link to="/" className="item">
<div>Status: {this.retrieveUserStatus()}</div> // highlight-line
</Link>
)
}
}
export default GoogleAuth
Listen For Authentication State Change
This is all great. However currently the component is only checking the status of the users authentication the first time the component renders. Therefore our "Status: XXXX" will not live update until the page is reloaded. How can we listen for changes to the authentication status? Google has a built in method for just this.
gapi.auth2.getAuthInstance().isSignedIn.listen()
The listen()
method takes a callback function as an argument, which will run every time the users status changes. Therefore we want to create a function that will update the isSignedIn
state to whatever the current status is, every time the status changes.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
state = { isSignedIn: null }
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
// create auth variable
this.auth = window.gapi.auth2.getAuthInstance()
// update state so that component will re-render
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
// listen for changes to authentication status
this.auth.isSignedIn.listen(this.onAuthChange) // highlight-line
})
})
}
// triggered when authentication status changes
// updates auth state to current auth status
onAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
}
// helper function
displayUserStatus() {
if (this.state.isSignedIn === null) {
return "UNKNOWN"
} else if (this.state.isSignedIn) {
return "SIGNED IN"
} else {
return "NOT SIGNED IN"
}
}
render() {
return (
<Link to="/" className="item">
<div>Status: {this.displayUserStatus()}</div>
</Link>
)
}
}
export default GoogleAuth
And every time we change our login status using the console the status display is now live updating without reloading the DOM.
Sign In/Out Buttons
Now we can replace our little text status with some sign in/out buttons.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
state = { isSignedIn: null }
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
// create auth variable
this.auth = window.gapi.auth2.getAuthInstance()
// update state so that component will re-render
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
// listen for changes to authentication status
this.auth.isSignedIn.listen(this.onAuthChange)
})
})
}
// updates auth state to current auth status
// triggered when authentication status changes
onAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
}
// render google login/out button
renderAuthButton() {
if (this.state.isSignedIn === null) {
return null
} else if (this.state.isSignedIn) {
return (
<button className="ui red google button">
<i className="google icon" />
Sign Out
</button>
)
} else {
return (
<button className="ui red google button">
<i className="google icon" />
Sign In
</button>
)
}
}
render() {
return (
<Link to="/" className="item">
<div>{this.renderAuthButton()}</div> // highlight-line
</Link>
)
}
}
export default GoogleAuth
They don't actually do anything yet, so we need to add some click event handlers to them.
import React, { Component } from "react"
import { Link } from "react-router-dom"
class GoogleAuth extends Component {
state = { isSignedIn: null }
componentDidMount() {
window.gapi.load("auth2", () => {
window.gapi.auth2
.init({
client_id: process.env.REACT_APP_GOOGLE_OAUTH2_CLIENT_ID,
scope: "email",
})
.then(() => {
// create auth variable
this.auth = window.gapi.auth2.getAuthInstance()
// update state so that component will re-render
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
// listen for changes to authentication status
this.auth.isSignedIn.listen(this.onAuthChange)
})
})
}
// updates auth state to current auth status
// triggered when authentication status changes
onAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() })
}
onSignInClick = () => {
this.auth.signIn()
}
onSignOutClick = () => {
this.auth.signOut()
}
// helper function
renderAuthButton() {
if (this.state.isSignedIn === null) {
return null
} else if (this.state.isSignedIn) {
return (
<button onClick={this.onSignOutClick} className="ui red google button"> // highlight-line
<i className="google icon" />
Sign Out
</button>
)
} else {
return (
<button onClick={this.onSignInClick} className="ui red google button"> // highlight-line
<i className="google icon" />
Sign In
</button>
)
}
}
render() {
return (
<Link to="/" className="item">
<div>{this.renderAuthButton()}</div>
</Link>
)
}
}
export default GoogleAuth
And now we will toggle logging in and logging out every time we press the button.
Next we need to integrate Redux into this authentication process so that we can keep track of our users authentication status globally, instead of just on pages where the GoogleAuth component exists. This post is getting long though so we will handle that in the next article.
GitHub Repo
Ncoughlin: React-Streams-Client
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.