Integrating Auth0 in a React App with an ASP.NET Core API backend

Alastair Christian
DataDIGEST
Published in
10 min readMay 13, 2020

It is increasingly common to use an external service to handle the authentication of users for a modern application. In most cases this makes a lot of sense. Why roll your own authentication when you can use a specialised service with more experts focussing on security than you will ever likely have in your team? One of the services in this field is provided by Auth0. In this post I will walk through from beginning to end the steps needed to integrate Auth0 into a React App that is communicating with an ASP.NET Core API backend, all running locally on your dev machine.

The code to accompany this post is available on GitHub.

Objectives

  1. Make certain routes in our client application protected, so that a user needs to be logged in to view them.
  2. Protect our API, so that calls to it require a valid bearer token.

Overview

This is a long post, broken into 6 steps.

  1. Create and Configure an Auth0 Tenant
  2. Make the Auth0 Settings Available in the Client App and API
  3. Integrate Auth0 in the React Client
  4. Restrict Access to Routes in the React Client
  5. Require a Token to Access Protected API Endpoints
  6. Obtain and Provide a Token When Accessing the API from the React Client

Starting Point

This illustration will modify the project that is created from the dotnet new react template. At the time of writing I was using the following versions of major components.

.NET: .NETCoreApp 3.1

React: 16.13.1

React-Router-Dom: 4.2.2

There are two organisational changes I made to the folder structure provided by the template.

  1. I moved the API calls out of the React components and into dedicated files under an api subfolder of my clientApp folder.
  2. I moved the ASP.NET MVC Models into a Models folder.
The final project structure

If you are following along, I assume that you have already signed up for a free Auth0 account at auth0.com.

Step 1: Create and Configure an Auth0 Tenant

The first step in adding Auth0 authentication to our application is to create a tenant in Auth0. We then need to create an Application and an API within the tenant. A tenant in Auth0 will be the entity that contains your applications and users. It is a good idea to have a separate tenant for each application and environment combination you support. For example, if you have a MyTodoApp, in addition to the MyTodoApp tenant you might have MyTodoApp-Dev and MyTodoApp-Test tenants to keep your dev and test users separate from production ones.

To create a tenant, drop down your account menu on the Auth0 dashboard and click on Create Tenant.

The Auth0 Account Menu

Give your new tenant a name and choose where the data will be hosted and click on Create. Once the tenant has been created you will see the Getting Started page.

Auth0 Getting Started Page

This can guide you through all the steps you are likely going to want to do on the Auth0 side, from adding applications to customising the Login box. Given the guidance on this page is so thorough I am not going to repeat it. What we need for our example is to create an Application and an API with the following settings.

Application settings

The application in this instance is our client app. If a setting is not mentioned below, then it can be left as its default value.

If you have configured your development web server to listen on a different port you will need to specify that port number instead of 5001.

API Settings

Step 2: Make the Auth0 settings Available in the API and Client App

Before leaving Auth0, we need to get the values for the settings we will need to provide when integrating Auth0 into our client app and API. These settings are:

From the Application:

  • Domain (from the Application)
  • ClientId

From the API:

  • Identifier (to be used as the audience parameter on authorization calls).

I like to keep my client and server configuration in one place, so I tend to place these values in my server side application settings (or more usually my project secrets during development). This means that I need to provide an endpoint for the client app to retrieve these settings when it first loads. The settings json will look like.

{"Auth":  {    "Domain": "{Domain}"    "Authority": "https://{Domain}"    "ClientId": "{ClientId}"    "Audience": "{Identifier}"  }}

“Authority” is a convenience setting that will be used when configuring our ASP.NET Core API for authentication. It simply saves me appending https: manually to the domain in the configuration code.

I then have a PublicAuthSettingsModel class to contain the settings that the client app requires.

An instance of this class is returned from the GetPublicAuthSettings action on a SettingsController.

The client app can now retrieve these settings by making a GET request to https://localhost:5001/api/auth. You could skip this of course and simply provide the Auth0 settings directly in your client app.

Step 3: Integrate Auth0 in the React Client

We are now ready to integrate Auth0 into our client app. The objective of this step is to be able to register, login, logout and change our password using Auth0. If you click on your client Application in the Auth0 tenant you will see that once again Auth0 provide a detailed Quick Start to guide you through these steps and provide the code that you will need to manually add to your client app.

Auth0 React Application Getting Started Page

I will summarise the relevant ones here.

Install Auth0’s JavaScript SDK for SPAs

Make sure you execute the following command from your ClientApp folder within the .NET project.

npm install @auth0/auth0-spa-js

The Quick Start also installs react-router-dom but this should already be present if you’ve used the dotnet new react template.

Create react-router’s history instance

Create a javascript file named history.js in ClientApp/src/utils. Add the following code to this file to make react-routers history module available so that we can perform redirects programmatically during login, etc.

Install the Auth0 React wrapper

Auth0 provide the code to create a React Provider to handle the interactions with the Auth0 SDK. Copy the code from your application’s Quick Start into a new file named react-auth0-spa.js in the ClientApp/src folder. At the time of creating this sample the code was as follows.

Modify ClientApp/src/index.js

With all our dependencies in place, we can add the code that finally integrates Auth0 into our client app. The final code for index.js is shown below, but to summarise the changes:

  • We add an onRedirectCallback function that we will pass to the Auth0Provider;
  • We call the API to get our public auth settings;
  • We wrap the App with the Auth0Provider.

Add a Login and Logout button

Auth0 is now in place in our client app, but we need a way to trigger logging in or out of the application. Let’s add a Login and Logout button to the NavMenu (ClientApp/src/components/NavMenu.js).

First, we need to import the useAuth0 context from the Auth0 React Wrapper.

import { useAuth0 } from “../react-auth0-spa”;

Next, we need to be able to access the state and functions we need from Auth0 within our NavMenu component.

const { isAuthenticated, loginWithRedirect, logout } = useAuth0();

If a user isn’t logged into the app, we want to display a login button, so we will conditionally display a NavItem containing this button.

Similarly, if a user is logged in, we want to display a logout button.

And that’s it. If you run the app now you should be able to click on the Login button, register a user via the Auth0 lock box and then login and logout with that user.

Step 4: Restrict Access to the FetchData Route in the React Client

We can now log users in and out of the app, but it doesn’t mean much because all the functionality of the app is available regardless of whether the user is authenticated or not. To illustrate how we can restrict access to parts of the client app based on the authentication status of a user, we will restrict access to the FetchData page so that only logged in users can view it.

The first thing we can do is not display the Fetch Data item in the NavMenu if a user is not authenticated. We’ve already done something similar for the Logout button, so let’s add to that by moving the Fetch Data NavItem into our isAuthenticated conditional block.

Now we can start up the app and see that we can’t see the Fetch Data item in the Nav until we log in. But of course this is really just window dressing. A user could still bookmark (or manually enter) https://localhost:5001/fetch-data and get access to this page. What we need to do is check whether the user is logged in when they try and access this page and redirect them to login if they aren’t. A common pattern for introducing this ability is to wrap routes that need to be protected in a PrivateRoute component which performs this check and redirect.

There are many implementations of PrivateRoute out there, but the code below is what works reliably for me. It is placed in a new PrivateRoute.js file in ClientApp/src/components/common.

Then, in App.js, instead of using Route for the FetchData component, we use PrivateRoute. We also wrap the routes with Switch so that only one match is returned, otherwise you find you get prompted to login as soon as the app loads, not when you try and visit FetchData.

Now when you run the app, you will find that you need to be logged in to visit https://localhost:5001/fetch-data, regardless of how you try and get there.

Step 5: Require a Token to Access the FetchData API

The client app is now limiting access to the FetchData component so that only logged in users can view the data. But if someone were to make a request to the API endpoint that the FetchData component uses (https://localhost:5001/api/WeatherForecast), the response would still include all the data. We need to prevent this by requiring a token to access the WeatherForecast endpoint.

Install the Microsoft.AspNetCore.Authentication.JwtBearer package

The first step is to add support for OpenID Connect tokens via the middlewear in the Microsoft.AspNetCore.Authentication.JwtBearer nuget package.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

Register Authentication as one of our API Application’s Services

In the Startup class’s ConfigureServices method, we need to add the authentication service and configure it to be based on bearer tokens issued by Auth0. The code to do this is as follows.

The values we are providing for the Authority and Audience are coming from the configuration we setup in Step 2. Authority is the address of the server that issued the token, which in our case will be https:{Domain} for our Auth0 configuration. Audience is the intended recipient of the token. Only tokens whose aud field matches this value will be accepted for authentication purposes.

Add Authentication and Authorization Middleware to the Request Pipeline

Having configured how we want our authentication to work we now need to make sure it is part of the HTTP Request Pipeline. To do this we simply call UseAuthentication on the IAppBuilder in the Startup Class’s Configure method.

We want to protect API methods with the [Authorize] attribute which requires the inclusion of the Authorization middleware. The Authorization middleware must be added between the Routing and Endpoints middleware.

Add the Authorize attribute to the WeatherForecastController

Now that we have our middleware configured to require a bearer token for protected endpoints, we simply need to add the Authorize attribute to any Controller or Action method we wish to protect. In our example, we add it to the WeatherForecastController so that any actions on this controller will require a bearer token be presented.

Step 6: Send a Bearer Token from the React Client when Calling FetchData

Our WeatherForecast endpoint is now protected and if we attempt to view the FetchData component in our Client App nothing will be displayed. We need to obtain a token from Auth0 and then provide it with any calls to the WeatherForecast endpoint.

Modify the GET Request to api/weatherForecast

Our API requires that a valid token be provided when requesting the weatherForecast. The token is provided in the Authorization header of the request. To achieve this we modify the weatherForecastApi.get function to accept the token as a parameter and provide it with the request. Our weatherForecastApi.js file now looks like.

Obtain a token from Auth0 and provide it to the weatherForecastApi.get() Function

Now we just need to request a token from Auth0 somehow. Fortunately, the Auth0Provider we’ve already added makes this easy. In our FetchData component we need to do the following.

First, we need to import the useAuth0 context from the Auth0 React Wrapper.

import { useAuth0 } from “../react-auth0-spa”;

Next, we need to be able to access the getTokenSilently function we need from Auth0 within our component.

const { getTokenSilently } = useAuth0();

Now, within populateWeatherData, instead of just immediately calling the weatherForecastApi.get() function, we first get the token and pass it along.

And that is it. If we run the app again, login and head to the Fetch Data page we’ll see that the weather forecast is once again obtained from the API and displayed.

Conclusion

Using a specialist authentication service instead of rolling your own authentication for every application you develop saves you time. It delivers a more robust and secure solution for your clients and users too. In this post we walked through how to protect both our React client application and ASP.NET Core API using authentication services provided by Auth0. Whilst there are a number of steps that have to be followed to integrate Auth0 into the solution and configure both the client and API to require and use authentication, the process is straightforward compared to doing this from scratch yourself.

References

The following resources were used in compiling this guide.

  1. OpenID Connect | OpenID
  2. Auth0: SPA + API
  3. Auth0 Online Course: Securing React Apps | Pluralsight
  4. Securing ASP.NET Core 3 with OAuth2 and OpenID Connect | Pluralsight

--

--