Securing a Chat App With React and Auth0

Ayooluwa I.
Ayooluwa I.
Published January 6, 2020 Updated April 28, 2022

In this tutorial, we will build a chat application that’ll allow users to participate in a group discussion similar to how channels work in Slack. We’ll handle user authentication, and management using Auth0's Authentication-as-a-Service solution that allows developers to add authentication to any application without breaking a sweat easily.

At the end of this article, you will have a working application that looks like this:

Auth0 Gif

Prerequisites

Before you proceed with this tutorial, make sure you have Node.js and npm installed on your machine. You also need to have a basic familiarity with building Node.js and React applications. However, no prior experience with Stream Chat or Auth0 is necessary.

Sign up for Stream

Go here to create a free Stream account or login to your existing account. Once you’re logged in, you can save the application access keys from the app that's automatically created for you. Or, if you prefer, go to the dashboard, hit the blue “Create App” button at the top right of the screen and give your app a name as shown below:

Image shows an app being created in the Stream dashboard

Once you have created your application, you will be presented with your application access keys, which we’ll be making use of soon.

Image shows access keys in the stream dashboard

Set up the Chat Server

Fire up the terminal on your machine, and create a new directory for this tutorial. cd into it and run npm init -y to initialize a new Node.js project. We’ll be using express to set up a simple server for generating tokens that will be used for authenticating users. To install all the necessary dependencies which we’ll be using on the server, run the command below from the project root:

npm install express cors body-parser dotenv stream-chat --save

Once the dependencies have been installed, create a .env file and paste your Stream application credentials into the file in the following format:

// .env
STREAM_API_KEY=<YOUR_API_KEY>
STREAM_APP_SECRET=<YOUR_APP_SECRET>

Following that, create a new server.js file in your project root, and enter the following code into it:

// server.js

require("dotenv").config();

const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const { StreamChat } = require("stream-chat");

const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const serverSideClient = new StreamChat(
  process.env.STREAM_API_KEY,
  process.env.STREAM_APP_SECRET
);

app.post("/join", async (req, res) => {
  const { username } = req.body;
  const token = serverSideClient.createToken(username);
  try {
    await serverSideClient.updateUser(
      {
        id: username
      },
      token
    );

    const admin = { id: "admin" };
    const channel = serverSideClient.channel("team", "chat", {
      name: "Group messaging",
      created_by: admin
    });

    await channel.create();
    await channel.addMembers([username]);
  } catch (err) {
    res.status(500).json({ err: err.message });
    return;
  }

  return res.status(200).json({ user: { username }, token });
});

app.listen(7000, () => {
  console.log(`Server running on PORT 7000`);
});

In the above file, we have a single /join route, which expects a username from the client-side and creates an authentication token for the user. The call to updateUser() creates the user on our Stream chat instance, passing in the token for the user. By default, user tokens are valid indefinitely. You can set an expiration on a token by passing the number of seconds till expiration as the second parameter.

After creating the user, we initialize a channel of the type team, whose id is set to chat. The team chat type is just one of the five default channel types on Stream Chat and provides a good number of defaults for group messaging applications. The other chat types are livestream, messaging, gaming, and commerce. You can also create your own types for specialized use cases.

Using the create() method, we set the creator of the channel to an admin user. Lastly, we add the newly registered user as a member of the channel before sending the authentication token back to the client. Now, whenever a user registers within our application, they’ll automatically be added to the channel.

Before moving on to the next section, make sure to start the server on port 7000 by running node server.js in the terminal.

Set up a New React App

Let’s create a new React application to showcase an exhibition of Stream’s React chat components, which makes building a custom chat solution a breeze.

Run npx create-react-app client in your project root to set up a new React project and install all the necessary dependencies. Next, cd into the newly created client directory, and run npm start to start the development server. This should open up a new tab in your browser at http://localhost:3000.

Sign up for Auth0

As mentioned earlier, we’ll be using Auth0 to authenticate users before they’re able to join our chat application. Head over to the Auth0 website and sign up for a free account. Once you’re logged in, you will be asked to choose a Tenant Domain and the region where your data will be hosted. Following that, you’ll need to create your first application.

Applications

To do so, hit the CREATE APPLICATION button and give your application a name. Also, select the Single Page Web Application option under Application type. Finally, create the Create button to create the application.

Auth0

Once your app is created, find the Settings tab and enter your application URL (http://localhost:3000) into the fields labeled Allowed Callback URLs, Allowed Web Origins, and Allowed Logout URLs.

Stream Chat Credentials

Allowed Callback URLs is a whitelist of URLs that the application is allowed to redirect to – for example, https://getstream.io/chat/redirect?foo=bar. Failure to set this will prevent users from being logged in. Likewise, Allowed Logout URLs is a whitelist of the URLs that the application can redirect to when someone logs out. Setting your application URL under Allowed Web Origins allows the application to refresh the authentication tokens automatically, otherwise, users will be logged out immediately they refresh the page.

Set up Authentication for Your React App

From within the client directory, run the command below to install Auth0’s JavaScript SDK for Single Page Applications:

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
$ npm install @auth0/auth0-spa-js

Following that, create an auth0.js file in the src directory, and populate it with the following code:

import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";

const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
export const Auth0Provider = ({
  children,
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  ...initOptions
}) => {
  const [isAuthenticated, setIsAuthenticated] = useState();
  const [user, setUser] = useState();
  const [auth0Client, setAuth0] = useState();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const initAuth0 = async () => {
      try {
        const auth0FromHook = await createAuth0Client(initOptions);
        setAuth0(auth0FromHook);

        if (window.location.search.includes("code=")) {
          const { appState } = await auth0FromHook.handleRedirectCallback();
          onRedirectCallback(appState);
        }

        const isAuthenticated = await auth0FromHook.isAuthenticated();
        setIsAuthenticated(isAuthenticated);

        if (isAuthenticated) {
          const user = await auth0FromHook.getUser();
          setUser(user);
        }

        setLoading(false);
      } catch (error) {
        console.log(error);
      }
    };

    initAuth0();
  }, []);

  const handleRedirectCallback = async () => {
    setLoading(true);
    await auth0Client.handleRedirectCallback();
    const user = await auth0Client.getUser();
    setLoading(false);
    setIsAuthenticated(true);
    setUser(user);
  };
  return (
    <Auth0Context.Provider
      value={{
        isAuthenticated,
        user,
        loading,
        handleRedirectCallback,
        loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
        logout: (...p) => auth0Client.logout(...p)
      }}
    >
      {children}
    </Auth0Context.Provider>
  );
};

The above code provides a set of React hooks that allow you to log a user in or out. We’ll explore how to use these hooks in the next section.

One more thing to do here is to wrap our components with the Auth0Provider component created above. This makes any component inside this wrapper able to access the Auth0 SDK client.

Open client/src/index.js and change it to look like this:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Auth0Provider } from "./auth0";

ReactDOM.render(
  <Auth0Provider
    domain={"<your domain>"}
    client_id={"<your client id>"}
    redirect_uri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();

You can grab your domain and client ID from under the Settings tab in your Auth0 dashboard. Replace the placeholders above with the appropriate values.

Auth0 Settings

Sign up and Login
When a user lands on the application page, they should be redirected to a page where they can register as a new user or login as an existing user. To achieve this, update your client/src/App.js file as follows:

import React, { useEffect } from "react";
import "./App.css";
import { useAuth0 } from "./auth0";

function App() {
  useEffect(() => {}, []);

  const { loading, user, loginWithRedirect } = useAuth0();

  if (loading) {
    return <div>Loading...</div>;
  }

  return <div className="App">{!user && loginWithRedirect({})}</div>;
}

export default App;

And update the client/src/App.css file as well:

html {
	box-sizing: border-box;
}
 
 *, *::before, *::after {
 	box-sizing: inherit;
 	margin: 0;
 	padding: 0;
 }
 
 body {
 	background-color: #f3f3f3;
 }
 
 .logout {
 	border: none;
 	outline: none;
 	background: transparent;
 	cursor: pointer;
 }

In App.js,, user allows us to determine if a user is logged in or not. As long as a user is not set, the current user is prompted to register or login using Auth0’s Universal Login Page.

Auth0 Example

When the user is logged in, control returns to the application, and a user will now be set. As is, nothing will be displayed when a user logs in. Let’s change that by utilizing the user profile retrieved from Auth0 to create a user on our chat application in the next section.

Add Chat Functionality With Stream

Go ahead and run the following command to install the additional dependencies needed to create our chat application. In addition to Stream’s API Client, we’ll be making use of ready-made React components that make setting up a beautiful and functional chat interface a breeze.

$ npm install stream-chat stream-chat-react axios

Next, create a new Chat.js file in client/src and populate it with the following code:

import React, { useState, useEffect } from "react";
import {
  Chat,
  Channel,
  Thread,
  Window,
  ChannelList,
  ChannelListTeam,
  MessageList,
  MessageTeam,
  MessageInput,
  withChannelContext
} from "stream-chat-react";
import { StreamChat } from "stream-chat";
import axios from "axios";
import { useAuth0 } from "./auth0";

import "stream-chat-react/dist/css/index.css";

const chatClient = new StreamChat("beyu2z5xwnea");

function ChatView() {
  const [channel, setChannel] = useState(null);
  const [loading, setLoading] = useState(false);
  const { user, logout } = useAuth0();

  // Usernames can only contain alphabets, numbers, underscores and dashes
  const username = user.email.replace(/([^a-z0-9_-]+)/gi, "_");

  useEffect(() => {
    async function getToken() {
      setLoading(true);
      let token;
      try {
        const response = await axios.post("http://localhost:7000/join", {
          username
        });
        token = response.data.token;
      } catch (err) {
        console.log(err);
        return;
      }

      chatClient.setUser(
        {
          id: username,
          name: user.nickname
        },
        token
      );

      const channel = chatClient.channel("team", "group-messaging-2");

      try {
        await channel.watch();
      } catch (err) {
        console.log(err);
        return;
      }

      setChannel(channel);
      setLoading(false);
    }

    getToken();
  }, [setLoading, user.email, user.name, user.nickname, username]);

  if (loading || !user) {
    return <div>Loading chat...</div>;
  }

  if (channel) {
    const CustomChannelHeader = withChannelContext(
      class CustomChannelHeader extends React.PureComponent {
        render() {
          return (
            <div className="str-chat__header-livestream">
              <div className="str-chat__header-livestream-left">
                <p className="str-chat__header-livestream-left--title">
                  {this.props.channel.data.name}
                </p>
                <p className="str-chat__header-livestream-left--members">
                  {Object.keys(this.props.members).length} members,{" "}
                  {this.props.watcher_count} online
                </p>
              </div>
              <div className="str-chat__header-livestream-right">
                <div className="str-chat__header-livestream-right-button-wrapper">
                  <button
                    className="logout"
                    onClick={() =>
                      logout({
                        returnTo: "http://localhost:3000/"
                      })
                    }
                  >
                    Logout
                  </button>
                </div>
              </div>
            </div>
          );
        }
      }
    );

    return (
      <Chat client={chatClient} theme="team light">
        <ChannelList
          options={{
            subscribe: true,
            state: true
          }}
          List={ChannelListTeam}
        />
        <Channel channel={channel}>
          <Window>
            <CustomChannelHeader />
            <MessageList Message={MessageTeam} />
            <MessageInput focus />
          </Window>
          <Thread Message={MessageTeam} />
        </Channel>
      </Chat>
    );
  }

  return null;
}

export default ChatView;

At the very top, we’ve imported a few components from the stream-chat-react package. The <Chat /> component acts as a wrapper and passes ChatContext to all other components.
<ChannelList /> is used to render a list of channels so you can select which one to join while <Channel /> acts as a wrapper component for a channel. To render basic information about a channel – the list of messages in the channel and the text input – the <CustomChannelHeader />, <MessageList />, and <MessageInput />, components are used respectively.

Although Stream has a default ChannelHeader component which you can use, a custom one is employed here to add a logout button that is not present in the default header.

By making use of Stream’s React UI Components, you get a lot of basic and advanced chat features for free such as typing indicators, emoji, reactions, file support, rich link preview, user presence (online or offline) and more which makes it easy to add in-app chat.

Update the App Component

The final step for this tutorial involves using the ChatView component in App.js as shown below:

import React, { useEffect } from "react";
import "./App.css";
import { useAuth0 } from "./auth0";
import ChatView from "./Chat";

function App() {
  useEffect(() => {}, []);

  const { loading, user, loginWithRedirect } = useAuth0();

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div className="App">
      {!user && loginWithRedirect({})}

      {user && <ChatView />}
    </div>
  );
}

export default App;

This indicates that when a user is set (post authentication), the ChatView component is rendered and the user is logged into our chat instance as seen in the GIF below.

Stream Chat Side by Side

Wrapping Up

In this tutorial, we explored how to make a secure chat application using Auth0, Stream Chat, and React. As you can see, building a secure user authentication flow in your Chat application is not hard at all with the help of Auth0’s robust solutions, and Stream’s React components make implementing complex chat features trivial.

You can build on the knowledge gained from this tutorial to build a chat solution that solves a real-world problem. You can check out other functionality Stream Chat offers by viewing its extensive documentation.

The complete code for this tutorial can be found on GitHub.

Happy chatting! 💬

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->