Look for the 🛠️️ emoji if you'd like to skim through the content while focusing on the build steps.

Auth0 launched its React SDK on June 24th, 2020, to provide React developers with an easier way to add user authentication to React applications using a hooks-centric approach. The Auth0 React SDK provides a high-level API to handle a lot of authentication implementation details. You can now secure your React applications using best practices while writing less code.

How does Auth0 work? You first integrate your application with Auth0. Your application will then redirect users to an Auth0 customizable login page when they need to log in. Once your users log in successfully, Auth0 redirects them back to your app, returning JSON Web Tokens (JWTs) with their authentication and user information.

The focus of this tutorial is to teach developers how to secure a React application by implementing user authentication. You'll enhance a starter React application to practice the following security concepts:

  • Add user login and logout.
  • Retrieve user information.
  • Protect application routers against unauthorized access.
  • Call secured endpoints from an API.

Auth0 React sample app

⏰⚡️ If you are short of time, checkout the Auth0 React Quickstart to get up and running with user authentication for React in just a few minutes. You may also check out our React and Auth0 YouTube Playlist.

Get the Starter Application

We have created a starter project using create-react-app to help you learn React security concepts through hands-on practice. The starter application uses React Bootstrap to take care of the layout and UI components. It also uses a custom Bootstrap theme created by the Auth0 Design Team to make the UI look nice.

🛠 As such, clone the auth0-react-sample repository on its starter branch to get started:

git clone -b starter git@github.com:auth0-blog/auth0-react-sample.git

🛠 Once you clone the repo, make auth0-react-sample your current directory:

cd auth0-react-sample

🛠 Install the React project dependencies:

yarn install

Connect React with Auth0

The best part of the Auth0 platform is how streamlined it is to get started by following these steps:

Sign up and create an Auth0 Application

🛠 If you haven't already, sign up for a free Auth0 account. A free account offers you:

During the sign-up process, you create something called an Auth0 Tenant, representing the product or service to which you are adding authentication.

🛠 Once you sign in, Auth0 takes you to the Dashboard. In the left sidebar menu, click on "Applications".

🛠 Then, click the "Create Application" button. A modal opens up with a form to provide a name for the application and choose its type.

  • Name:
Auth0 React SDK Sample
  • Application Type: Single Page Web Applications

🛠 Click the "Create" button to complete the process. Your Auth0 application page loads up.

In the next step, you'll learn how to help React and Auth0 communicate.

What's the relationship between Auth0 Tenants and Auth0 Applications? Let's say that you have a photo-sharing React app called "Reactogram". You then would create an Auth0 tenant called reactogram. From a customer perspective, Reactogram is that customer's product or service.

Now, say that Reactogram is available on three platforms: web as a single-page application and as a native mobile app for Android and iOS. If each platform needs authentication, you need to create three Auth0 applications to provide the product with everything it needs to authenticate users through that platform.

Reactogram users would belong to the Auth0 Reactogram tenant, which shares them across its Auth0 applications. If you were to launch another product called "Reactiktok" that needs authentication, you would need to create another tenant, reactiktok, and create Auth0 applications to support the platforms where it lives.

Create a communication bridge between React and Auth0

When you use Auth0, you don't have to build login forms. Auth0 offers a Universal Login page to reduce the overhead of adding and managing authentication.

How does Universal Login work?

Your React application will redirect users to Auth0 whenever they trigger an authentication request. Auth0 will present them with a login page. Once they log in, Auth0 will redirect them back to our application. For that redirecting to happen securely, you must specify in your Auth0 Application Settings the URLs to which Auth0 can redirect users once it authenticates them.

Watch a video on how Auth0 Universal Login works.

🛠 As such, click on the "Settings" tab of your Auth0 Application page and fill in the following values:

🛠 Allowed Callback URL

http://localhost:3000

The above value is the URL that Auth0 can use to redirect your users after they successfully log in.

🛠 Allowed Logout URL

http://localhost:3000

The above value is the URL that Auth0 can use to redirect your users after they log out.

🛠 Allowed Web Origins

http://localhost:3000

The above value is the URL that Auth0 can use with Cross-Origin Authentication.

🛠 Scroll down and click the "Save Changes" button.

🛠 Do not close this page yet. You'll need some of its information in the next section.

Add the Auth0 configuration variables to React

From the Auth0 Application, you need configuration variables to allow your React application to use the previously created communication bridge. Within the Auth0 Application Settings page, you need the Auth0 Domain and Client ID.

What exactly is an Auth0 Domain and an Auth0 Client ID?

Domain

When you created a new Auth0 account, Auth0 asked to pick a name for your Tenant. This name, appended with auth0.com, is your Auth0 Domain. It's the base URL that you will use to access the Auth0 APIs and the URL where you'll redirect users to log in.

You can also use custom domains to allow Auth0 to do the authentication heavy lifting for you without compromising your branding experience.

Client ID

Each application is assigned a Client ID upon creation, which is an alphanumeric string, and it's the unique identifier for your application (such as q8fij2iug0CmgPLfTfG1tZGdTQyGaTUA). You cannot modify the Client ID. You will use the Client ID to identify the Auth0 Application to which the Auth0 React SDK needs to connect.

Warning: Another important piece of information present in the "Settings" is the Client Secret. This secret protects your resources by only granting tokens to requestors if they're authorized. Think of it as your application's password, which must be kept confidential at all times. If anyone gains access to your Client Secret, they can impersonate your application and access protected resources.

🛠 Open the React starter project, auth0-react-sample, and create a .env file under the project directory:

touch .env

🛠 Populate .env as follows:

REACT_APP_AUTH0_DOMAIN=
REACT_APP_AUTH0_CLIENT_ID=

🛠 The value of REACT_APP_AUTH0_DOMAIN is the "Domain" value from the "Settings".

🛠 The value of REACT_APP_AUTH0_CLIENT_ID is the "Client ID" value from the "Settings".

Auth0 Application Settings from the Auth0 Dashboard

These variables let your React application identify itself as an authorized party to interact with the Auth0 authentication server.

Auth0 and React connection set

You have completed setting up an authentication service that your React application can consume. All that is left is for you to continue building up the starter project throughout this tutorial.

Feel free to dive deeper into the Auth0 Documentation to learn more about how Auth0 helps you save time on implementing and managing identity.

Set Up the Auth0 React SDK

🛠 You need to follow these steps to integrate the Auth0 React SDK with your React application.

Install the Auth0 React SDK

🛠 Execute the following command:

yarn add @auth0/auth0-react

Configure the Auth0Provider component

Under the hood, the Auth0 React SDK uses React Context. The SDK uses an Auth0Context component to manage the authentication state of your users. In turn, the SDK exposes the Auth0Provider component that provides that Auth0Context to its child components. As such, you can wrap your root component, such as App, with Auth0Provider to integrate Auth0 with your React app.

<Auth0Provider>
  <App />
</Auth0Provider>

However, user authentication is a mechanism to monitor who is accessing your application and control what they can do. For example, you can prevent users who have not logged in from accessing parts of your application. In that scenario, Auth0 can act as your application bouncer.

A bouncer is a person employed by a nightclub or similar establishment to prevent troublemakers from entering or to eject them from the premises. React security is not too different from nightclub security.

If users want to enter a private route from your application, Auth0 will stop them and ask them to present their credentials. If Auth0 can verify who they are and that they are supposed to go in there, Auth0 will let them in. Otherwise, Auth0 will take them back to a public application route.

Now, it's important to reiterate that the authentication process won't happen within your application layer. Your React application will redirect your users to the Auth0 Universal Login page, where Auth0 asks for credentials and redirects the user back to your application with the result of the authentication process.

The Auth0Provider remembers where the user wanted to go and, if authentication were successful, it takes the user to that route. As such, the Auth0Provider needs to have access to the session history of the application. The starter React app uses React Router to manage its routing. React Router exposes a React Hook that makes it easy for you to access the session history through a history object, useHistory().

Consequently, you need to wrap the Auth0Provider with BrowserRouter from React Router, which uses a RouterContext.Provider component under the hood to maintain routing state:

<BrowserRouter>
  <Auth0Provider>
    <App />
  </Auth0Provider>
</BrowserRouter>

Here, what you see at play is a pillar of React's architecture: you extend components, not through inheritance but composition.

How do you create an Auth0Provider with access to the application session history?

🛠 Create an auth0-provider-with-history.js file under the src directory to define an Auth0ProviderWithHistory component, which uses composition to make React Router Hooks available to Auth0Provider:

touch src/auth0-provider-with-history.js

🛠 Populate src/auth0-provider-with-history.js with the following:

import React from "react";
import { useHistory } from "react-router-dom";
import { Auth0Provider } from "@auth0/auth0-react";

const Auth0ProviderWithHistory = ({ children }) => {
  const domain = process.env.REACT_APP_AUTH0_DOMAIN;
  const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;

  const history = useHistory();

  const onRedirectCallback = (appState) => {
    history.push(appState?.returnTo || window.location.pathname);
  };

  return (
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      redirectUri={window.location.origin}
      onRedirectCallback={onRedirectCallback}
    >
      {children}
    </Auth0Provider>
  );
};

export default Auth0ProviderWithHistory;

What is happening within Auth0ProviderWithHistory?

  • You need the Auth0 React SDK to connect with the correct Auth0 Application to process authentication. As such, you need to Auth0 Domain and Client ID to configure the Auth0Provider.

  • You use the onRedirectCallback() method to handle the event where Auth0 redirects your users from the Auth0 Universal Login page to your React application. You use the useHistory() hook to get the history object from React Router. You use the history.push() method to take users back to the route they intended to access before authentication.

That's it! Wrapping any component tree with Auth0ProviderWithHistory will give it access to the Auth0Context.

How do you use Auth0ProviderWithHistory?

The Auth0ProviderWithHistory requires the BrowserRouter component from React Router to be its parent, grandparent, or great-great-great-grandparent.

The Context from React Router must be present in the component tree at a higher level for Auth0ProviderWithHistory to get access to the useHistory() hook from React Router.

🛠 Open src/index.js and update it as follows to build the proper component tree to power the routing and user authentication features of your React application:

import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import { BrowserRouter as Router } from "react-router-dom";
import Auth0ProviderWithHistory from "./auth0-provider-with-history";

import "./index.css";

ReactDOM.render(
  <Router>
    <Auth0ProviderWithHistory>
      <App />
    </Auth0ProviderWithHistory>
  </Router>,
  document.getElementById("root")
);

🛠 Execute the following command to run your React application:

yarn start

The Auth0 React SDK is all set up. You are ready to implement user authentication in the next section.

Add User Authentication

You need to provide UI elements for your users to trigger authentication events: login, logout, and sign up.

Create a login button

🛠 Create a login-button.js file under the src/components/ directory:

touch src/components/login-button.js

🛠 Populate src/components/login-button.js like so:

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

const LoginButton = () => {
  const { loginWithRedirect } = useAuth0();
  return (
    <Button
      onClick={() => loginWithRedirect()}
      variant="primary"
      className="btn-margin"
    >
      Log In
    </Button>
  );
};

export default LoginButton;

loginWithRedirect() is a method exposed by the Auth0Context. This method redirects to the Auth0 /authorize endpoint to carry out the authentication process.

You can pass parameters to the /authorize URL by passing an object argument to loginWithRedirect(). Every property of that object argument becomes a parameter of the /authorize URL, allowing you to further define and customize its behavior. See RedirectLoginOptions for more details on these options.

For example, you can pass options to loginWithRedirect() to redirect users to an Auth0 Universal Login page optimized for signing up for your React application.

Create a sign-up button

You can make users land directly on a sign-up page instead of a login page by specifying the screen_hint=signup parameter when redirecting to /authorize. Thus, you need to pass the following object to the loginWithRedirect() method to create that parameter:

{
  screen_hint: "signup",
}

🛠 Create a signup-button.js file under the src/components/ directory:

touch src/components/signup-button.js

🛠 Populate src/components/signup-button.js like so to define a SignupButton component:

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

const SignupButton = () => {
  const { loginWithRedirect } = useAuth0();
  return (
    <Button
      onClick={() =>
        loginWithRedirect({
          screen_hint: "signup",
        })
      }
      variant="primary"
      className="btn-margin"
    >
      Sign Up
    </Button>
  );
};

export default SignupButton;

Using the Signup feature requires you to enable the Auth0 New Universal Login Experience in your tenant.

🛠 Open the Universal Login section of the Auth0 Dashboard and choose the "New" option under the "Experience" subsection.

Auth0 Universal Login Experience options

🛠 Scroll down and click on the "Save Changes" button.

The difference between the LoginButton and SignupButton user experience will be more evident once you integrate those components with your React application and see them in action. You'll do that in the next sections.

Create a logout button

🛠 Create a logout-button.js file under the src/components/ directory:

touch src/components/logout-button.js

Populate src/components/logout-button.js like so:

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

const LogoutButton = () => {
  const { logout } = useAuth0();
  return (
    <Button
      onClick={() =>
        logout({
          returnTo: window.location.origin,
        })
      }
      variant="danger"
      className="btn-margin"
    >
      Log Out
    </Button>
  );
};

export default LogoutButton;

The logout() method exposed by Auth0Context clears the application session and redirects to the Auth0 /v2/logout endpoint to clear the Auth0 session. As with the login methods, you can pass an object argument to logout() to define parameters for the /v2/logout call. See LogoutOptions for mode details.

Here, you pass the returnTo option to specify the URL where Auth0 should redirect your users after they logout. Right now, you are working locally and your Auth0 application's "Allowed Logout URLs" point to http://localhost:3000.

However, if you were to deploy your React application to production, you need to add the production logout URL to the "Allowed Logout URLs" list and ensure that Auth0 redirects your users to that production URL and not localhost. Setting returnTo to window.location.origin will do just that.

Read more about how Logout works at Auth0.

Integrate the login and logout buttons

🛠 Open nav-bar.js under the src/components/ directory and perform the following steps:

  • 🛠 Update the module imports to bring in the useAuth0() hook and the Button component as follows:
import React from "react";
import { NavLink as RouterNavLink } from "react-router-dom";
import { Container, Nav, Navbar } from "react-bootstrap";

import { useAuth0 } from "@auth0/auth0-react";
import LogoutButton from "./logout-button";
import LoginButton from "./login-button";
  • 🛠 Locate the AuthNav component and update it like so:
const AuthNav = () => {
  const { isAuthenticated } = useAuth0();

  return (
    <Nav className="justify-content-end">
      {isAuthenticated ? <LogoutButton /> : <LoginButton />}
    </Nav>
  );
};

isAuthenticated is a boolean value exposed by the Auth0Context. Its value is true when Auth0 has authenticated the user and false when it hasn't.

🛠 Go ahead and try to log in. Your React application redirects you to the Auth0 Universal Login page. You can use a form to log in using a username and password or a social identity provider like Google. Notice that this login page also gives you the option to sign up.

New Auth0 Universal Login Experience Form

Experiment: Use the SignupButton component If you swap the LoginButton component with the SignupButton component in the ternary operation, you'll land in a page with language optimized to encourage you to sign up for your application. Try this out!

New Auth0 Universal Login Experience Signup Page

You can customize the appearance of New Universal Login pages. You also can override any text in the New Experience by using the Text Customization API.

Notice that when you finish logging in and Auth0 redirects you to your React app, the login button briefly shows up (blue color), and then the logout button renders (red color).

The user interface flashes because your React app doesn't know if Auth0 has authenticated the user yet. Your app will know the user authentication status after the Auth0 React SDK loads.

🛠 To fix that UI flashing, use the isLoading boolean value exposed by the Auth0Context to render the App component once the React SDK has finished loading.

🛠 Open src/app.js and update it as follows:

import React from "react";
import { Route, Switch } from "react-router-dom";
import { Container } from "react-bootstrap";
import { useAuth0 } from "@auth0/auth0-react";

import { NavBar, Footer, Loading } from "./components";
import { Home, Profile, ExternalApi } from "./views";

import "./app.css";

const App = () => {
  const { isLoading } = useAuth0();

  if (isLoading) {
    return <Loading />;
  }

  return (
    <div id="app" className="d-flex flex-column h-100">
      <NavBar />
      <Container className="flex-grow-1 mt-5">
        <Switch>
          <Route path="/" exact component={Home} />
          <Route path="/profile" component={Profile} />
          <Route path="/external-api" component={ExternalApi} />
        </Switch>
      </Container>
      <Footer />
    </div>
  );
};

export default App;

While the SDK is loading, the Loading component, which has a cool animation, renders.

Retrieving user information

After a user successfully logs in, Auth0 sends an ID token to your React application. Authentication systems, such as Auth0, use ID Tokens in token-based authentication to cache user profile information and provide it to a client application. The caching of ID tokens can contribute to improvements in performance and responsiveness for your React application.

You can use the data from the ID token to personalize the user interface of your React application. The Auth0 React SDK decodes the ID token and stores its data in the user object exposed by the Auth0Context. Some of the information present in the ID token includes the name, nickname, picture, and email of the logged-in user.

How can you use the ID token to create a profile page for your users?

🛠 Update the Profile component in src/views/profile.js as follows:

import React from "react";
import { Container, Row, Col } from "react-bootstrap";
import { Highlight } from "../components";

import { useAuth0 } from "@auth0/auth0-react";

const Profile = () => {
  const { user } = useAuth0();
  const { name, picture, email } = user;

  return (
    <Container className="mb-5">
      <Row className="align-items-center profile-header mb-5 text-center text-md-left">
        <Col md={2}>
          <img
            src={picture}
            alt="Profile"
            className="rounded-circle img-fluid profile-picture mb-3 mb-md-0"
          />
        </Col>
        <Col md>
          <h2>{name}</h2>
          <p className="lead text-muted">{email}</p>
        </Col>
      </Row>
      <Row>
        <Highlight>{JSON.stringify(user, null, 2)}</Highlight>
      </Row>
    </Container>
  );
};

export default Profile;

What's happening within the Profile component?

  • You destructure the user object to obtain the user name, picture, and email.

  • You then display these three properties in the user interface. Since the data comes from a simple object, you don't have to fetch it using any asynchronous calls.

  • Finally, you display the full content of the decoded ID token within a code box. You can now see all the other properties available for you to use.

The Profile component renders user information that you could consider private. Additionally, the user property is null if there is no logged-in user. So either way, this component should only render if Auth0 has authenticated the user.

As such, you should protect the route that renders this component, http://localhost:3000/profile. You'll learn how to do just that in the next section.

Protecting Routes

The Auth0 React SDK exposes a withAuthenticationRequired Higher-Order Component (HOC) that you can use to protect routes. You can also use withAuthenticationRequired to create a PrivateRoute component to protect routes in a more declarative way using React Router.

Use a Higher-Order Component to protect a route

🛠 Open src/views/profile.js and update it as follows:

import React from "react";
import { Container, Row, Col } from "react-bootstrap";
import { Highlight, Loading } from "../components";

import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";

const Profile = () => {
  const { user } = useAuth0();
  const { name, picture, email } = user;

  return (
    <Container className="mb-5">
      <Row className="align-items-center profile-header mb-5 text-center text-md-left">
        <Col md={2}>
          <img
            src={picture}
            alt="Profile"
            className="rounded-circle img-fluid profile-picture mb-3 mb-md-0"
          />
        </Col>
        <Col md>
          <h2>{name}</h2>
          <p className="lead text-muted">{email}</p>
        </Col>
      </Row>
      <Row>
        <Highlight>{JSON.stringify(user, null, 2)}</Highlight>
      </Row>
    </Container>
  );
};

export default withAuthenticationRequired(Profile, {
  onRedirecting: () => <Loading />,
});

When you wrap your components in the withAuthenticationRequired Higher-Order Component and users who have not logged in visit a page that renders that component, your React application will redirect that user to the login page. After the user logs in, Auth0 will redirect the user to your React application, and the Auth0Provider will take the users to the page they intended to access before login.

withAuthenticationRequired takes the following arguments:

The options you can specify are loginOptions to pass additional parameters to the Auth0 /authorize endpoint and returnTo to specify a path for React to redirect the users after the login transaction that the user triggered in this component completes.

Perhaps the most crucial option is onRedirecting. This option lets you render a component while your React application redirects the user to the login page. In the example above, users who have not logged in see the Loading component as soon they hit the /profile route. The onRedirecting component improves the user experience by avoiding any flashing of mixed UI components (private and public components).

What if you are using React Router?

Using withAuthenticationRequired to wrap the component directly is not the most declarative way to build a React application. If you were to look at the routes defined in the App component, you wouldn't be able to tell which routes are private and which routes are public.

const App = () => {
  const { isLoading } = useAuth0();

  if (isLoading) {
    return <Loading />;
  }

  return (
    <div id="app" className="d-flex flex-column h-100">
      <NavBar />
      <Container className="flex-grow-1 mt-5">
        <Switch>
          <Route path="/" exact component={Home} />
          <Route path="/profile" component={Profile} />
          <Route path="/external-api" component={ExternalApi} />
        </Switch>
      </Container>
      <Footer />
    </div>
  );
};

🛠 Instead of using withAuthenticationRequired directly, you can wrap it in a PrivateRoute component that leverages the features of React Router.

Create a private route component to protect a route

The starter application uses React Router as its routing library. This example applies only to that library.

In this section, you'll create PrivateRoute component that uses the Route component from React Router to render the withAuthenticationRequired Higher-Order Component. The advantage of this approach is that your PrivateRoute will have the same API as an out-of-the-box Route component. As such, you can compose PrivateRoute with other React Router components organically.

🛠 To start, create a private-route.js file under the src/components directory:

touch src/components/private-route.js

🛠 Populate src/components/private-route.js as follows:

import React from "react";
import { Route } from "react-router-dom";
import { withAuthenticationRequired } from "@auth0/auth0-react";
import { Loading } from "./index";

const PrivateRoute = ({ component, ...args }) => (
  <Route
    component={withAuthenticationRequired(component, {
      onRedirecting: () => <Loading />,
    })}
    {...args}
  />
);

export default PrivateRoute;

🛠 Now, update src/components/index.js, which uses a barrel to export all the modules within components:

import Content from "./content";
import Footer from "./footer";
import Hero from "./hero";
import Loading from "./loading";
import NavBar from "./nav-bar";
import Highlight from "./highlight";
import PrivateRoute from "./private-route";

export { Content, Footer, Hero, Loading, NavBar, Highlight, PrivateRoute };

🛠 Finally, open the src/app.js file. Locate the Switch component and change the Route component for the /profile and /external-api path to the PrivateRoute component:

import React from "react";
import { Route, Switch } from "react-router-dom";
import { Container } from "react-bootstrap";
import { useAuth0 } from "@auth0/auth0-react";

import { NavBar, Footer, Loading, PrivateRoute } from "./components";
import { Home, Profile, ExternalApi } from "./views";

import "./app.css";

const App = () => {
  const { isLoading } = useAuth0();

  if (isLoading) {
    return <Loading />;
  }

  return (
    <div id="app" className="d-flex flex-column h-100">
      <NavBar />
      <Container className="flex-grow-1 mt-5">
        <Switch>
          <Route path="/" exact component={Home} />
          <PrivateRoute path="/profile" component={Profile} />
          <PrivateRoute path="/external-api" component={ExternalApi} />
        </Switch>
      </Container>
      <Footer />
    </div>
  );
};

export default App;

You don't need to use the withAuthenticationRequired HOC directly in the Profile component any longer.

🛠 Open src/views/profile.js and revert the file to its previous code content:

import React from "react";
import { Container, Row, Col } from "react-bootstrap";
import { Highlight } from "../components";

import { useAuth0 } from "@auth0/auth0-react";

const Profile = () => {
  const { user } = useAuth0();
  const { name, picture, email } = user;

  return (
    <Container className="mb-5">
      <Row className="align-items-center profile-header mb-5 text-center text-md-left">
        <Col md={2}>
          <img
            src={picture}
            alt="Profile"
            className="rounded-circle img-fluid profile-picture mb-3 mb-md-0"
          />
        </Col>
        <Col md>
          <h2>{name}</h2>
          <p className="lead text-muted">{email}</p>
        </Col>
      </Row>
      <Row>
        <Highlight>{JSON.stringify(user, null, 2)}</Highlight>
      </Row>
    </Container>
  );
};

export default Profile;

🛠 You can now test that these two paths require users to authenticate before they can access them. Log out and try to access the Profile or External API tab. If it works, React redirects you to log in with Auth0.

Client-side guards improve the user experience of your React application, not its security.

In Security StackExchange, Conor Mancone explains that server-side guards are about protecting data while client-side guards are about improving user experience.

The main takeaways from his response are:

  • You can't rely on client-side restrictions, such as navigation guards and private routes, to protect sensitive information.
    • Attackers can potentially get around client-side restrictions.
  • Your server should not return any data that a user should not access.
    • Returning all the user data from the server and letting the front-end framework decide what to display and what to hide based on the user authentication status is the wrong approach.
    • Anyone can open the browser's developer tools and inspect the network requests to view all the data.
  • The use of navigation guards helps improve user experience, not user security.
    • Without guards, a user who has not logged in may wander into a page with restricted information and see an error, like "Access Denied".
    • With guards that match the server permissions, you can prevent users from seeing errors by preventing them from visiting the restricted page.

Which route protection strategy would you prefer to use in your React applications? Please, let me know in the comments below.

Calling an API

This section focuses on showing you how to get an access token in your React application and how to use it to make secure API calls.

When you use Auth0, you delegate the authentication process to a centralized service. Auth0 provides you with functionality to log in and log out users from your React application. However, your application may need to access resources from a secured API.

You can also protect an API with Auth0. Auth0 offers multiple Auth0 API quickstarts to help you integrate Auth0 with your backend platform.

When you use Auth0 to protect your API, you delegate the authentication process to a centralized service that ensures only approved applications can access private resources. Your React application authenticates the user and receives an access token from Auth0.

The application can then pass that access token to your API as a credential. In turn, your API can use Auth0 libraries to verify the access tokens it receives from the calling application and issue a response with the desired data.

Instead of creating a demo API to test the client-server connection, you'll use a demo Express API that I've prepared for you.

Get the demo Express API

🛠 Open a new terminal window and clone the auth0-express-sample repo somewhere in your system. Ensure that you clone it outside of your React project directory.

git clone git@github.com:auth0-blog/auth0-express-sample.git

🛠 Once you clone this repo, make the auth0-express-sample directory your current directory:

cd auth0-express-sample

🛠 Install the Node.js project dependencies:

npm install

Connect the Express API with Auth0

Create a communication bridge between Express and Auth0

This process is similar to how you connected React with Auth0.

🛠 Head to the APIs section in the Auth0 Dashboard, and hit the "Create API" button.

🛠 Then, in the form that Auth0 shows:

  • Add a Name to your API:
Auth0 Express Sample
  • Set its Identifier value:
https://express.sample
  • Leave the signing algorithm as RS256 as it's the best option from a security standpoint.

Auth0 Dashboard new API form

Identifiers are unique strings that help Auth0 differentiate between your different APIs. We recommend using URLs to facilitate creating unique identifiers predictably; however, Auth0 never calls these URLs.

🛠 With these values in place, hit the "Create" button.

Add the Auth0 configuration variables to Express

🛠 Now, click on the "Quick Start" tab of your Auth0 API page. This page presents instructions on how to set up different APIs. From the code box, choose "Node.js". Keep this page open as you'll be using the values next.

Auth0 API Node.js Quickstart

🛠 Create a .env file for the API Server under the auth0-express-sample directory:

touch .env

🛠 Populate this auth0-express-sample/.env file as follows:

API_PORT=7000
APP_ORIGIN=http://localhost:3000
AUTH0_AUDIENCE=
AUTH0_ISSUER=

🛠 Head back to the "Node.js" code snippet from the Auth0 API "Quick Start" page. Locate the definition of jwtCheck, which receives the return value of the jwt function. This function receives an object as an argument. Take a closer look at the properties of this object:

🛠 The value of AUTH0_AUDIENCE is the value of its audience property.

🛠 The value of AUTH0_ISSUER is the value of its issuer property.

Do not include the quotes, only the string value.

🛠 With the API configuration values set, run the API server by issuing the following command:

npm start

Configure React to connect with the Express API

🛠 Head back to the auth0-react-sample project directory that stores your React application.

🛠 Locate the auth0-react-sample/.env file and add an audience and an API URL value to it:

REACT_APP_AUTH0_DOMAIN=YOUR-AUTH0-DOMAIN
REACT_APP_AUTH0_CLIENT_ID=YOUR-AUTH0-APP-CLIENT-ID
REACT_APP_AUDIENCE=https://express.sample
REACT_APP_API_URL=http://localhost:7000

🛠 The value of REACT_APP_AUDIENCE is the same as AUTH0_AUDIENCE from auth0-express-sample/.env.

Your React application needs to pass an access token when it calls a target API to access private resources. You can request an access token in a format that the API can verify by passing the audience and scope props to Auth0Provider.

🛠 Restart your React application so that it can use the new values you've set in auth0-react-sample/.env.

🛠 Update the auth0-provider-with-history.js file under the auth0-react-sample/src directory to add the audience prop:

import React from "react";
import { useHistory } from "react-router-dom";
import { Auth0Provider } from "@auth0/auth0-react";

const Auth0ProviderWithHistory = ({ children }) => {
  const history = useHistory();
  const domain = process.env.REACT_APP_AUTH0_DOMAIN;
  const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
  const audience = process.env.REACT_APP_AUDIENCE;

  const onRedirectCallback = (appState) => {
    history.push(appState?.returnTo || window.location.pathname);
  };

  return (
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      redirectUri={window.location.origin}
      onRedirectCallback={onRedirectCallback}
      audience={audience}
    >
      {children}
    </Auth0Provider>
  );
};

export default Auth0ProviderWithHistory;

Why is the Auth0 Audience value the same for both apps? Auth0 uses the value of the audience prop to determine which resource server (API) the user is authorizing your React application to access.

The actions that your React application can perform on the API depend on the scopes that your access token contains, which you define as the value of a scope props in Auth0Provider.

Remember that screen you saw when you first logged in with Auth0 asking you for permission to access your profile information? Your React application will request authorization from the user to access the requested scopes, and the user will approve or deny the request.

When you don't pass a scope prop to Auth0Provider as in the example above, the React SDK defaults to the OpenID Connect Scopes: openid profile email.

  • openid: This scope informs the Auth0 Authorization Server that the Client is making an OpenID Connect (OIDC) request to verify the user's identity. OpenID Connect is an authentication protocol.

  • profile: This scope value requests access to the user's default profile information, such as name, nickname, and picture.

  • email: This scope value requests access to the email and email_verified information.

The details of the OpenID Connect Scopes go into the ID Token. In the case of your APIs, you'll define custom API scopes to implement access control, and you'll identify them in the calls that your client applications make to that API. Auth0 includes API scopes in the access token as the scope claim.

The concepts about API scopes or permissions are better covered in an Auth0 API tutorial such as "Use TypeScript to Create a Secure API with Node.js and Express: Role-Based Access Control".

🛠 Update src/views/external-api.js as follows:

import React, { useState } from "react";
import { Button, ButtonGroup, Container } from "react-bootstrap";
import { Highlight } from "../components";
import { useAuth0 } from "@auth0/auth0-react";

export const ExternalApi = () => {
  const [message, setMessage] = useState("");
  const apiUrl = process.env.REACT_APP_API_URL;

  const { getAccessTokenSilently } = useAuth0();

  const callApi = async () => {
    try {
      const response = await fetch(`${apiUrl}/api/public-message`);

      const responseData = await response.json();

      setMessage(responseData);
    } catch (error) {
      setMessage(error.message);
    }
  };

  const callSecureApi = async () => {
    try {
      const token = await getAccessTokenSilently();

      const response = await fetch(`${apiUrl}/api/private-message`, {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      const responseData = await response.json();

      setMessage(responseData);
    } catch (error) {
      setMessage(error.message);
    }
  };

  return (
    <Container className="mb-5">
      <h1>External API</h1>
      <p>
        You use will use a button to call an external API using an access token,
        and the API will validate it using the API's audience value.{" "}
        <strong>This route should be private</strong>.
      </p>
      <ButtonGroup>
        <Button onClick={callApi} color="primary" className="mt-5">
          Get Public Message
        </Button>
        <Button onClick={callSecureApi} color="primary" className="mt-5">
          Get Private Message
        </Button>
      </ButtonGroup>

      {message && (
        <div className="mt-5">
          <h6 className="muted">Result</h6>
          <Highlight>{JSON.stringify(message, null, 2)}</Highlight>
        </div>
      )}
    </Container>
  );
};

export default ExternalApi;

What is happening now within the ExternalApi component?

  • You add a callApi() method that performs a public API request.

  • You add a callSecureApi() method that performs a secure API request by (a) getting the access token from Auth0 and (b) passing that access token as a bearer credential in the authorization header of the request.

  • You use the useState() React hook to update the user interface whenever any of the described API calls complete successfully.

Your previous login request did not include an audience parameter. As such, the React SDK doesn't have an access token stored in memory.

You should not store tokens in localStorage. Why? Storing tokens in browser local storage provides persistence across page refreshes and browser tabs, however if an attacker can run JavaScript in the Single-Page Application (SPA) using a cross-site scripting (XSS) attack, they can retrieve the tokens stored in local storage.

A vulnerability leading to a successful XSS attack can be either in the SPA source code or in any third-party JavaScript code included in the SPA, such as Bootstrap, jQuery, or Google Analytics.

🛠 Log out and log back in to get a new access token from Auth0. Visit http://localhost:3000/external-api and press the buttons in the External API page to test the responses.

Conclusion

You have implemented user authentication in React to identify your users, get user profile information, and control the content that your users can access. This tutorial covered the most common authentication use cases for a React application. However, Auth0 is an extensible and flexible platform that can help you achieve even more.

In a follow-up guide, we'll cover advanced authentication patterns and tooling, such as using a pop-up instead of a redirect to log in users, adding permissions to ID tokens, using metadata to enhance user profiles, and much more.

Let me know in the comments below what you thought of this tutorial. Thank you for reading and stay tuned, please.