TL;DR: In this tutorial, I’ll show you how to build a secure chat app with React and Pusher using the Auth0 authentication service and a Node + Express Backend Server. We will use Auth0 to authenticate users so they can access the chat feature of a website.
Auth0 is a platform that allows developers add authentication to their applications easily. It offers products like Centralized Login Page, Lock, Passwordless Authentication, Multifactor Authentication and Breached Password Detection. It’s a service that helps to eliminate the headaches of authentication when building modern applications.
Pusher is a platform that builds realtime and scalable infrastructures for developers so you can spend more time building awesome features. Pusher offers features like Presence Channels, Pub/Sub Messaging and Access Control.
As we go through the tutorial, we will be using these two awesome services to build an application that authenticates users via Auth0 and also features a group chat thanks to Pusher.
Here’s a preview of what we will be building.
An understanding of Javascript (ES6) and React is needed for this tutorial. An excellent primer on using React with Auth0 can be seen here.
React Up - Creating the React App
We’ll be using the create-react-app command to bootstrap the app. The create-react-app CLI was built by the Facebook team to help users of React (especially beginners) start a React app with zero configurations. It helps to scaffold a React app with zero build configuration and just works out of the box.
Let’s install create-react-app and also scaffold a new React app so we can start building our chat app. We do that with the following commands:
yarn add create-react-app
create-react-app react-pusher
Once that’s completed, you can cd
in to the react-pusher directory and run yarn start
to start the app and see it on localhost:3000
but before we do that, let’s install some dependencies. We’ll be using these dependencies later as we build our chat app:
yarn add auth0-js bootstrap events \
history react-bootstrap react-router \
react-router-dom axios pusher-js
So, why do we need these dependencies?
auth0-js
is the client side Javascript toolkit for the Auth0 API and how we connect to the Auth0 service/dashboard.- Bootstrap is a CSS framework and it comes with CSS features that will help in styling the chat app. It’s also needed for the
react-bootstrap
module. events
is Node's event emitter for all engines.history
is a JavaScript library that lets you easily manage session history anywhere JavaScript runs and we’ll be using it in our routes to manage navigation.react-bootstrap
is a library of reusable front-end components built with Bootstrap.react-router
andreact-router-dom
helps with routing in our React app.
At this point, your project directory should be very similar to the image below.
Open up index.js
inside the src
folder and edit with the following code:
import ReactDOM from 'react-dom';
import './index.css';
import 'bootstrap/dist/css/bootstrap.css';
import { makeMainRoutes } from './routes';
const routes = makeMainRoutes();
ReactDOM.render(
routes,
document.getElementById('root')
);
We basically just imported ReactDOM, the index.css
file, Bootstrap CSS framework, and a routes
file that we will create later.
Now that we are done with scaffolding the React app, we can leave it and come back to it after setting up Pusher.
Setting Up Pusher
Setting up Pusher means logging in to your dashboard (or creating a free account if you don’t already have one) and creating a new app. Copy your app_id, key, secret and cluster and store them somewhere as we’ll be needing them later.
Setting Up The Node Server
As mentioned above, we’ll need to create a Node.js server. We’ll be using Express as the Node.js framework. Let’s install the dependencies needed.
yarn add express pusher body-parser nodemon
The server will require an entry point so create a server.js
file and type in the following code:
// server.js
const express = require('express');
const path = require('path');
const bodyParser = require("body-parser");
const app = express();
const Pusher = require('pusher');
//initialize Pusher with your appId, key and secret
const pusher = new Pusher({
appId: 'APP_ID',
key: 'APP_KEY',
secret: 'SECRET',
cluster: 'YOUR CLUSTER',
encrypted: true
});
// Body parser middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// API route which the chat messages will be sent to
app.post('/message/send', (req, res) => {
// 'private' is prefixed to indicate that this is a private channel
pusher.trigger( 'private-reactchat', 'messages', {
message: req.body.message,
username: req.body.username
});
res.sendStatus(200);
});
// API route used by Pusher as a way of authenticating users
app.post('/pusher/auth', (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
const auth = pusher.authenticate(socketId, channel);
res.send(auth);
});
// Set port to be used by Node.js
app.set('port', (process.env.PORT || 5000));
app.listen(app.get('port'), function() {
console.log('Node app is running on port', app.get('port'));
});
In the code block above, Pusher is initialized with the dashboard credentials, and the various API routes are also defined. Now, let’s get back to building the ReactJS app.
ReactJS App
Remember, we already scaffolded the ReactJS app s to make sure everything works fine, run the following command in the root of your project folder:
yarn start
That will run the required scripts necessary and your ReactJS app should work now at http://localhost:3000
Let's begin adding Auth0 to the React app.
Set Up Auth0
We’ll be using Auth0 to authenticate the users so go to auth0.com and create an account. Once you are done creating the new account, you will be prompted to create a new client, so create one and name it anything you want or just use the default app. Take note of your client details in the settings tab.
Adding a Callback URL
One more thing you'll need to configure on your Auth0 dashboard is the callback URL. A callback URL is a URL that your ReactJS application will redirect to after a successful authentication from Auth0. Under the settings tab in the Auth0 Dashboard, set your callback URL to http://localhost:3000/callback
.
Writing Auth0 Code
Inside the src
folder, create a folder Auth
. In that folder, we'll create two JS files called Auth.js
and auth0-variables.js
.
The auth0-variables.js
will contain our Auth0 credentials. Open the auth0-variables.js
file and type in the following code:
export const AUTH_CONFIG = {
domain: 'yourname.auth0.com',
clientId: 'xxxxxxxxxxxxxx',
callbackUrl: 'http://localhost:3000/callback' //Callback URL set in the Auth0 dasbhoard.
}
Remember to edit with the credentials from your Auth0 dashboard. Next, open the Auth.js
file and type in the following code:
// imports EventEmitter
import { EventEmitter } from 'events';
// imports the Auth0 JS library
import auth0 from 'auth0-js';
// imports Auth0 credentials from the auth0-variables.js file
import { AUTH_CONFIG } from './auth0-variables';
// imports the history module, which will be created later
import history from '../history';
export default class Auth extends EventEmitter {
// An instance of Auth0 is instantiated with Auth0 credentials gotten from the auth0-variables.js file
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
// Telling Auth0 what to return after a successful authentication, in this case, the token and the id_token
responseType: 'token id_token',
// To retrieve a user's profile after authentication, we need to add openid profile to the the scope.
scope: 'openid profile'
});
// Local variable to hold a user's profile after authentication
userProfile;
// The methods below are bound in the constructor with 'this'
constructor() {
super();
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
this.getAccessToken = this.getAccessToken.bind(this);
this.getProfile = this.getProfile.bind(this);
}
login() {
this.auth0.authorize();
}
handleAuthentication() {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
history.replace('/home');
} else if (err) {
history.replace('/home');
console.log(err);
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
}
setSession(authResult) {
if (authResult && authResult.accessToken && authResult.idToken) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
// navigate to the home route
history.replace('/home');
}
}
getAccessToken() {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
throw new Error('No access token found');
}
return accessToken;
}
getProfile(cb) {
let accessToken = this.getAccessToken();
this.auth0.client.userInfo(accessToken, (err, profile) => {
if (profile) {
this.userProfile = profile;
localStorage.username = profile.nickname;
}
cb(err, profile);
});
}
logout() {
// Clear access token and ID token from local storage
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
this.userProfile = null;
// navigate to the home route
history.replace('/home');
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
}
Let's go over the Authentication service above. The basic gist of the code block is that when a user is successfully authenticated at Auth0's login/signup page, they are redirected back to your page and there will be a hash in the URL containing their authentication information. We have some methods above and I'll go over them and what they do:
login()
The login()
method calls the authorize function from auth0.js.
handleAuthentication()
The handleAuthentication()
method looks for a result after a successful authentication in the browser URL hash and processes it with the parseHash method from auth0.js.
setSession()
This method sets the user's access_token
, id_token
, and a time at which the access_token
will expire.
getAccessToken()
This method checks for an access_token
in the localStorage and throws an error if there's none.
getProfile(cb)
The getProfile()
method utilizes Auth0's clientInfo
which calls the /userinfo
endpoint and retrieves the user's information. An access_token
must be passed into the method as the first argument, and the second argument should have variables for error handling and to hold the user's profile. We then set the profile information to the userProfile
variable declared above.
logout()
The logout()
method removes the user's tokens from browser storage and effectively signs them out.
isAuthenticated()
The isAuthenticated()
method checks whether the expiry time for the access_token has passed.
In the code above, we imported an history.js
file which hadn't been created yet. history
was used in some of our methods above to help with navigation, so let's create that now. Create a file named history.js
inside the src
folder and type in the following code:
import createHistory from 'history/createBrowserHistory'
export default createHistory({
forceRefresh: true
})
Now that we have the Auth0 part down, let's begin to test if the Authentication service we wrote above actually works and also begin to build our UI.
Adding Routes
We'll have four different routes in this application.
- Home route -->
/
- Profile route -->
/profile
- Chat route -->
/chat
- Callback route -->
/callback
So basically, a page that serves as the homepage, a page that will be used to authenticate users, a page that shows the user's profile, a page where users can chat and lastly the callback page route in which users will be redirected to after authentication. Let's create a route file and also the components above.
Adding A Route file
Inside the src
folder, create a file named routes.js
and type in the following code:
import React from 'react';
import { Redirect, Route, BrowserRouter } from 'react-router-dom';
import App from './App';
import Auth from './Auth/Auth';
import history from './history';
// These components which will be created later will serve the various routes below
import Home from './Home/Home'; // The / route
import Profile from './Profile/Profile'; // The /profile route
import Chat from './Chat/Chat'; // The /chat route
import Callback from './Callback/Callback'; // The /callback route
//Instantiate the Auth0 service
const auth = new Auth();
// This function utilizes the handleAuthentication() method in Auth/Auth.js
const handleAuthentication = (nextState, replace) => {
if (/access_token|id_token|error/.test(nextState.location.hash)) {
auth.handleAuthentication();
}
}
// Routes are declared here and also exported for use in other components.
export const makeMainRoutes = () => {
return (
<BrowserRouter history={history} component={App}>
<div>
{/* '/' route*/}
<Route path="/" render={(props) => <App auth={auth} {...props} />} />
{/* 'Homepage' route*/}
<Route path="/home" render={(props) => <Home auth={auth} {...props} />} />
{/* 'Chat' route*/}
<Route path="/chat" render={(props) => (
!auth.isAuthenticated() ? (
<Redirect to="/home"/>
) : (
<Chat auth={auth} {...props} />
)
)} />
{/* 'Profile' route*/}
<Route path="/profile" render={(props) => (
!auth.isAuthenticated() ? (
<Redirect to="/home"/>
) : (
<Profile auth={auth} {...props} />
)
)} />
{/* 'Callback' route*/}
<Route path="/callback" render={(props) => {
handleAuthentication(props);
return <Callback {...props} />
}}/>
</div>
</BrowserRouter>
);
}
In the code block above, we declared the routes that we will be using in this ReactJS application, although the components for the routes are not created yet (that will be done soon).
One other thing we do in the code above is in the /profile
and /chat
route, as we check that a user is authenticated first before going to that route. If the user is not authenticated, they are automatically redirected to the /home
page.
Before we start writing code for the various components, let's edit the App.js
file inside the src
folder. Open the App.js
file and edit it with the following code:
// Import React and Component from React
import React, { Component } from 'react';
// Import the Navbar, Nav, Button component from the react-bootstrap
import { Navbar, Nav, Button } from 'react-bootstrap';
// Import the CSS styles from the App.css file
import './App.css';
class App extends Component {
// This function helps with navigation of different routes
goTo(route) {
this.props.history.replace(`/${route}`)
}
// This function calls on the auth login() function and logs in a user with Auth0
login() {
this.props.auth.login();
}
// This function calls on the auth logout() function and clears the localStorage thereby logging a user out.
logout() {
this.props.auth.logout();
}
render() {
// Destructuring assignment syntax is used to get the isAuthenticated function from the Authentication service in Auth.js
const { isAuthenticated } = this.props.auth;
return (
<div>
<Navbar className="no-border" fluid inverse>
<Navbar.Header>
<Navbar.Brand>
<a href="/home">ReactChat</a>
</Navbar.Brand>
</Navbar.Header>
<Nav className="pull-right">
<Button
className="btn-margin"
onClick={this.goTo.bind(this, 'home')}
>
Home
</Button>
{
!isAuthenticated() && (
<Button
className="btn-margin"
onClick={this.login.bind(this)}
>
Login
</Button>
)
}
{
isAuthenticated() && (
<Button
className="btn-margin"
onClick={this.goTo.bind(this, 'profile')}
>
Profile
</Button>
)
}
{
isAuthenticated() && (
<Button
className="btn-margin"
onClick={this.goTo.bind(this, 'chat')}
>
Chat
</Button>
)
}
{
isAuthenticated() && (
<Button
className="btn-margin"
onClick={this.logout.bind(this)}
>
Log Out
</Button>
)
}
</Nav>
</Navbar>
</div>
);
}
}
export default App;
The Login and Logout buttons make calls to the Auth service in the Auth.js
file via the onClick function attached to them so as to allow a user log in and out. These buttons will be shown based on the user's authentication state.
Therefore, when the Login button is clicked, the user will be redirected to Auth0's hosted login page.
Let's also edit the App.css
with the following:
.btn-margin {
margin: 7px 3px;
}
.no-border {
border-radius: 0px;
}
Adding a Home Component
Inside the src
folder, create a folder titled Home
, and in it create a file named Home.js
. Let's edit it with the following code:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class Home extends Component {
componentWillMount() {
const { isAuthenticated, getProfile } = this.props.auth;
if (isAuthenticated() ) {
getProfile();
}
}
login() {
this.props.auth.login();
}
render() {
const { isAuthenticated } = this.props.auth;
return (
<div className="container">
<div className="jumbotron">
<h1>Welcome to ReactChat!</h1>
{
!isAuthenticated() && (
<div>
<p>We need you to sign in/sign up with Auth0 before you can access our chat. 😁</p>
<p><a className="btn btn-primary btn-lg" onClick={this.login.bind(this)}>Login</a></p>
</div>
)
}
{
isAuthenticated() && (
<div>
<p>Let's chat. 😁</p>
<Link className="btn btn-primary btn-lg" to="chat">Chat</Link>
</div>
)
}
</div>
{this.props.children}
</div>
);
}
}
export default Home;
We are simply displaying a welcome section for the users on this page. The content of the welcome section is displayed conditionally depending on the user's current authentication state.
If a user is signed in, they see a button to begin chatting and if a user isn't signed in, they see a button to log in. We also used componentWillMount()
to get a user’s profile only if they are authenticated. This is important, as if we don’t do that, the Chat page would show an empty username on first load.
Adding a Callback Component
We mentioned the callback URL as a page the user sees after a successful authentication so let's create the page for that route.
Inside the src
folder, create a folder titled Callback
, and in it create a file named Callback.js
. Let's edit it with the following code:
import React, { Component } from 'react';
import loading from './loading.svg';
class Callback extends Component {
render() {
const style = {
position: 'absolute',
display: 'flex',
justifyContent: 'center',
height: '100vh',
width: '100vw',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
}
return (
<div style={style}>
<img src={loading} alt="loading"/>
</div>
);
}
}
export default Callback;
We’ll need a loading.svg
file of some sort. I’ve created one here which you can download and place inside the Callback
folder.
Adding a Profile Component
We want to be able to show a user’s profile and information gotten from Auth0, so let’s add the profile component.
Inside the src
folder, create a folder titled Profile
, and in it create a file named Profile.js
. Let's edit it with the following code:
import React, { Component } from 'react';
// Import Bootstrap components from react-bootstrap
import { Panel, ControlLabel, Glyphicon } from 'react-bootstrap';
// Import custom CSS from Profile.css
import './Profile.css';
class Profile extends Component {
// componentWillMount() is invoked immediately before mounting occurs and we are setting the profile state to the value gotten from getprofile() which is called from the Auth service in Auth.js.
componentWillMount() {
this.setState({ profile: {} });
const { userProfile, getProfile } = this.props.auth;
// Check if there's a user profile, if there's none use the getProfile method from Auth. js to get a a profile and set it to the profile state.
if (!userProfile) {
getProfile((err, profile) => {
this.setState({ profile });
});
} else {
this.setState({ profile: userProfile });
}
}
render() {
// Using destructuring assignment to set the constant profile to the state
const { profile } = this.state;
return (
<div className="container">
<div className="profile-area">
<h1>{profile.name}</h1>
<Panel header="Profile">
<img src={profile.picture} alt="profile" />
<div>
<ControlLabel><Glyphicon glyph="user" /> Nickname</ControlLabel>
<h3>{profile.nickname}</h3>
</div>
<pre>{JSON.stringify(profile, null, 2)}</pre>
</Panel>
</div>
</div>
);
}
}
export default Profile;
In the code above, we initially set the profile state inside the componentWillMount() method and we also check if there's a user profile. If there's not, use the getProfile method from Auth.js to get a a profile and set it to the profile state.
Inside the render()
function we use the profile state values to populate the view with the user’s information.
We’ll also need a Profile.css
inside the Profile
folder. Create a file with that name and edit with the following code:
.profile-area img {
max-width: 150px;
margin-bottom: 20px;
}
.panel-body h3 {
margin-top: 0;
}
The profile page should look like this:
Adding a Chat Component + Pusher
Let’s add the chat component so we can begin chatting. We will also integrate Pusher so we can see our chat messages update in realtime. Inside the src
folder, create a folder titled Chat
, and in it create a file named Chat.js
. Let's edit it with the following code:
import React, { Component } from 'react'
// Import CSS styles for Chat page
import './Chat.css'
// Import Bootstrap components from react-bootstrap
import { FormControl, Grid, Row, Col } from 'react-bootstrap';
// Import the axios library
import axios from 'axios'
// Import the Pusher JS library
import Pusher from 'pusher-js'
class Chat extends Component {
// The state is initialized in the constructor and the functions below are bound with 'this'.
constructor() {
super();
this.state = {
value: '',
username: '',
messages: []
};
this.sendMessage = this.sendMessage.bind(this);
this.handleChange = this.handleChange.bind(this);
}
// componentWillMount() is invoked immediately before mounting occurs and we are setting the username state to the value gotten from the localStorage.
componentWillMount() {
this.setState({ username: localStorage.username });
// Establish a connection to Pusher.
this.pusher = new Pusher('APP_KEY', {
authEndpoint: '/pusher/auth',
cluster: 'YOUR_CLUSTER',
encrypted: true
});
// Subscribe to the 'private-reactchat' channel
this.chatRoom = this.pusher.subscribe('private-reactchat');
}
// componentDidMount() is invoked immediately after a component is mounted. Listen for changes to the 'messages' state via Pusher and updates it.
componentDidMount() {
this.chatRoom.bind('messages', newmessage => {
this.setState({messages: this.state.messages.concat(newmessage)})
}, this);
}
// Used to update the value of the input form in which we type in our chat message
handleChange(event) {
this.setState({value: event.target.value});
}
// This sends the message inside the input form and sends it to Pusher.
sendMessage(event) {
event.preventDefault();
if (this.state.value !== '') {
axios.post('/message/send', {
username: this.state.username,
message: this.state.value
})
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error)
})
this.setState({value: ''})
}
else {
// console.log('enter message')
}
}
render() {
// Renders the chat messages
const messages = this.state.messages;
const message = messages.map(item => {
return (
<Grid>
{message}
<Row className="show-grid">
<Col xs={12}>
<div className="chatmessage-container">
<div key={item.id} className="message-box">
<p><strong>{item.username}</strong></p>
<p>{item.message}</p>
</div>
</div>
</Col>
</Row>
</Grid>
)
})
// Renders the input form where the message to be sent is typed.
return (
<Grid>
<Row className="show-grid">
<Col xs={12}>
{message}
<div className="chat-container">
<form onSubmit={this.sendMessage}>
<Col xs={5} xsOffset={3}>
<FormControl
type="text"
value={this.state.value}
placeholder="Enter message here"
onChange={this.handleChange}
/>
</Col>
<Col xs={4}>
<input className="btn btn-primary" value="Send" type="submit" />
</Col>
</form>
<h4 className="text-center">Welcome, {this.state.username}</h4>
<h5 className="text-center">Begin chatting here.</h5>
</div>
</Col>
</Row>
</Grid>
)
}
}
export default Chat;
In the code block above, we established a connection to Pusher with the APP_KEY
, created a function that uses the /message/send
API route to send the chat message to Pusher and we also used the componentDidMount()
method to listen to changes to the messages
state and automatically render it on the chat page.
Let’s create the the Chat.css
file and type in the following code:
.chat-container {
margin-top: 50px;
}
.chatmessage-container {
border: 1px solid #ccc;
border-radius: 5px;
margin: 20px auto;
width: 700px;
display: table;
}
.message-box {
background-color: #eee;
padding: 20px;
border-bottom: 1px solid #cccccc;
}
Before we run the app to test, we need to do one more thing.
API Proxying
Since we are also running a backend server, we need to find a way to run the React app and backend server together. API proxying helps with that. To tell the development server to proxy any unknown requests (/message/send
) to your API server in development, add a proxy
field to your package.json
immediately after the scripts
object.
"proxy": "http://localhost:5000"
We’ll also edit the scripts
object. Edit the scripts.start
key to this "nodemon server.js & react-scripts start"
. Your final package.json
file should look like this:
{
"name": "pusher-auth0",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "0.9.5"
},
"dependencies": {
"auth0-js": "^9.0.0",
"axios": "^0.16.2",
"body-parser": "^1.17.2",
"bootstrap": "^3.3.7",
"events": "^1.1.1",
"express": "^4.15.3",
"history": "^4.6.1",
"nodemon": "^1.11.0",
"pusher": "^1.5.1",
"pusher-js": "^4.1.0",
"react": "^15.5.4",
"react-bootstrap": "^0.31.0",
"react-dom": "^15.5.4",
"react-router": "^4.1.1",
"react-router-dom": "^4.1.1"
},
"scripts": {
"start": "nodemon server.js & react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:5000"
}
You can now run yarn start
in your terminal and see the app work on http://localhost:3000/home
.
Aside: Securing React Apps with Auth0
As you will learn in this section, you can easily secure your React applications with Auth0, a global leader in Identity-as-a-Service (IDaaS) that provides thousands of enterprise customers with modern identity solutions. Alongside with the classic username and password authentication process, Auth0 allows you to add features like Social Login, Multifactor Authentication, Passwordless Login, and much more with just a few clicks.
To follow along the instruction describe here, you will need an Auth0 account. If you don't have one yet, now is a good time to sign up for a free Auth0 account.
Also, if you want to follow this section in a clean environment, you can easily create a new React application with just one command:
npx create-react-app react-auth0
Then, you can move into your new React app (which was created inside a new directory called react-auth0
by the create-react-app
tool), and start working as explained in the following subsections.
Setting Up an Auth0 Application
To represent your React application in your Auth0 account, you will need to create an Auth0 Application. So, head to the Applications section on your Auth0 dashboard and proceed as follows:
- click on the Create Application button;
- then define a Name to your new application (e.g., "React Demo");
- then select Single Page Web Applications as its type.
- and hit the Create button to end the process.
After creating your application, Auth0 will redirect you to its Quick Start tab. From there, you will have to click on the Settings tab to whitelist some URLs that Auth0 can call after the authentication process. This is a security measure implemented by Auth0 to avoid the leaking of sensitive data (like ID Tokens).
So, when you arrive at the Settings tab, search for the Allowed Callback URLs field and add http://localhost:3000/callback
into it. For this tutorial, this single URL will suffice.
That's it! From the Auth0 perspective, you are good to go and can start securing your React application.
Dependencies and Setup
To secure your React application with Auth0, there are only three dependencies that you will need to install:
auth0.js
: This is the default library to integrate web applications with Auth0.react-router
: This is the de-facto library when it comes to routing management in React.react-router-dom
: This is the extension to the previous library to web applications.
To install these dependencies, move into your project root and issue the following command:
npm install --save auth0-js react-router react-router-dom
Note: As you want the best security available, you are going to rely on the Auth0 login page. This method consists of redirecting users to a login page hosted by Auth0 that is easily customizable right from your Auth0 dashboard. If you want to learn why this is the best approach, check the Universal vs. Embedded Login article.
After installing all three libraries, you can create a service to handle the authentication process. You can call this service Auth
and create it in the src/Auth/
directory with the following code:
// src/Auth/Auth.js
import auth0 from 'auth0-js';
export default class Auth {
constructor() {
this.auth0 = new auth0.WebAuth({
// the following three lines MUST be updated
domain: '<AUTH0_DOMAIN>',
audience: 'https://<AUTH0_DOMAIN>/userinfo',
clientID: '<AUTH0_CLIENT_ID>',
redirectUri: 'http://localhost:3000/callback',
responseType: 'token id_token',
scope: 'openid profile',
});
this.getProfile = this.getProfile.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.setSession = this.setSession.bind(this);
}
getProfile() {
return this.profile;
}
handleAuthentication() {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
if (err) return reject(err);
console.log(authResult);
if (!authResult || !authResult.idToken) {
return reject(err);
}
this.setSession(authResult);
resolve();
});
});
}
isAuthenticated() {
return new Date().getTime() < this.expiresAt;
}
login() {
this.auth0.authorize();
}
logout() {
// clear id token and expiration
this.idToken = null;
this.expiresAt = null;
}
setSession(authResult) {
this.idToken = authResult.idToken;
this.profile = authResult.idTokenPayload;
// set the time that the id token will expire at
this.expiresAt = authResult.expiresIn * 1000 + new Date().getTime();
}
}
The Auth
service that you just created contains functions to deal with different steps of the sign in/sign up process. The following list briefly summarizes these functions and what they do:
getProfile
: This function returns the profile of the logged-in user.handleAuthentication
: This function looks for the result of the authentication process in the URL hash. Then, the function processes the result with theparseHash
method fromauth0-js
.isAuthenticated
: This function checks whether the expiry time for the user's ID token has passed.login
: This function initiates the login process, redirecting users to the login page.logout
: This function removes the user's tokens and expiry time.setSession
: This function sets the user's ID token, profile, and expiry time.
Besides these functions, the class contains a field called auth0
that is initialized with values extracted from your Auth0 application. It is important to keep in mind that you have to replace the <AUTH0_DOMAIN>
and <AUTH0_CLIENT_ID>
placeholders that you are passing to the auth0
field.
Note: For the
<AUTH0_DOMAIN>
placeholders, you will have to replace them with something similar toyour-subdomain.auth0.com
, whereyour-subdomain
is the subdomain you chose while creating your Auth0 account (or your Auth0 tenant). For the<AUTH0_CLIENT_ID>
, you will have to replace it with the random string copied from the Client ID field of the Auth0 Application you created previously.
Since you are using the Auth0 login page, your users are taken away from the application. However, after they authenticate, users automatically return to the callback URL that you set up previously (i.e., http://localhost:3000/callback
). This means that you need to create a component responsible for this route.
So, create a new file called Callback.js
inside src/Callback
(i.e., you will need to create the Callback
directory) and insert the following code into it:
// src/Callback/Callback.js
import React from 'react';
import { withRouter } from 'react-router';
function Callback(props) {
props.auth.handleAuthentication().then(() => {
props.history.push('/');
});
return <div>Loading user profile.</div>;
}
export default withRouter(Callback);
This component, as you can see, is responsible for triggering the handleAuthentication
process and, when the process ends, for pushing users to your home page. While this component processes the authentication result, it simply shows a message saying that it is loading the user profile.
After creating the Auth
service and the Callback
component, you can refactor your App
component to integrate everything together:
// src/App.js
import React from 'react';
import { withRouter } from 'react-router';
import { Route } from 'react-router-dom';
import Callback from './Callback/Callback';
import './App.css';
function HomePage(props) {
const { authenticated } = props;
const logout = () => {
props.auth.logout();
props.history.push('/');
};
if (authenticated) {
const { name } = props.auth.getProfile();
return (
<div>
<h1>Howdy! Glad to see you back, {name}.</h1>
<button onClick={logout}>Log out</button>
</div>
);
}
return (
<div>
<h1>I don't know you. Please, log in.</h1>
<button onClick={props.auth.login}>Log in</button>
</div>
);
}
function App(props) {
const authenticated = props.auth.isAuthenticated();
return (
<div className="App">
<Route
exact
path="/callback"
render={() => <Callback auth={props.auth} />}
/>
<Route
exact
path="/"
render={() => (
<HomePage
authenticated={authenticated}
auth={props.auth}
history={props.history}
/>
)}
/>
</div>
);
}
export default withRouter(App);
In this case, you are actually defining two components inside the same file (just for the sake of simplicity). You are defining a HomePage
component that shows a message with the name of the logged-in user (that is, when the user is logged in, of course), and a message telling unauthenticated users to log in.
Also, this file is making the App
component responsible for deciding what component it must render. If the user is requesting the home page (i.e., the /
route), the HomePage
component is shown. If the user is requesting the callback page (i.e., /callback
), then the Callback
component is shown.
Note that you are using the Auth
service in all your components (App
, HomePage
, and Callback
) and also inside the Auth
service. As such, you need to have a global instance for this service, and you have to include it in your App
component.
So, to create this global Auth
instance and to wrap things up, you will need to update your index.js
file as shown here:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Auth from './Auth/Auth';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
const auth = new Auth();
ReactDOM.render(
<BrowserRouter>
<App auth={auth} />
</BrowserRouter>,
document.getElementById('root'),
);
registerServiceWorker();
After that, you are done! You just finished securing your React application with Auth0. If you take your app for a spin now (npm start
), you will be able to authenticate yourself with the help of Auth0, and you will be able to see your React app show your name (that is, if your identity provider does provide a name).
If you are interested in learning more, please, refer to the official React Quick Start guide to see, step by step, how to properly secure a React application. Besides the steps shown in this section, the guide also shows:
Conclusion
We’ve seen how to use ReactJS, Auth0 and Pusher to build a chat app. We also saw how to use the -private
channel and how to authenticate a user using the /pusher/auth
endpoint.
ReactJS works very well with Pusher because of its declarative, unidirectional data flow. You can see more examples on React and Pusher here and here.
The combination of Pusher and Auth0 can be extended to build wonderful use cases and applications. We could add social sign-ins with Auth0 and implement a “Who’s Online” feature thanks to Pusher. The possibilities are endless. You can learn more about both services by visiting their websites(Pusher and Auth0).
You can check the GitHub repository for the source code.
Yomi Eluwande is a frontend developer and product designer. He enjoys making products that solve problems. He is currently interested in the intersection between code and design.