Sign Up
Hero

Backend For Frontend Authentication Pattern with Auth0 and ASP.NET Core

Understand the Backend For Frontend authentication pattern and how it can be implemented in ASP.NET with Auth0.

TL;DR: This article discusses the Backend For Frontend authentication pattern and how it can be used in practice in SPAs implemented with React that use ASP.NET Core 5 as backend. Basic knowledge of the OAuth 2.0 and OpenID Connect is desirable but not required.

What Is the Backend For Frontend Authentication Pattern?

As you start looking into the different OAuth flows and the scenarios they cover, client type is one of those relevant aspects mentioned everywhere. The OAuth 2.0 specification defines two different client types, public and confidential clients, under section #2.1.

Public clients are those that run in places where secrets could be exposed as part of the source code or if the binaries are decompiled. These usually are single-page apps running in a browser or native apps running in user devices such as mobile phones or smart TVs.

On the other hand, confidential clients are the ones that can keep secrets in a private store, like, for example, a web application running in a web server, which can store secrets on the backend.

The client type will determine one or more OAuth flows suitable for the application implementation. By sticking to one of those flows, you can also lower the risks of getting the application compromised from an authentication and authorization standpoint.

The Backend For Frontend (a.k.a BFF) pattern for authentication emerged to mitigate any risk that may occur from negotiating and handling access tokens from public clients running in a browser. The name also implies that a dedicated backend must be available for performing all the authorization code exchange and handling of the access and refresh tokens. This pattern relies on OpenID Connect, which is an authentication layer that runs on top of OAuth to request and receive identity information about authenticated users.

This pattern does not work for a pure SPA that relies on calling external APIs directly from javascript or a serverless backend (e.g., AWS Lamba or Azure Functions).

The following diagram illustrates how this pattern works in detail:

  1. When the frontend needs to authenticate the user, it calls an API endpoint (/api/login) on the backend to start the login handshake.
  2. The backend uses OpenID connect with Auth0 to authenticate the user and getting the id, access, and refresh tokens.
  3. The backend stores the user's tokens in a cache.
  4. An encrypted cookie is issued for the frontend representing the user authentication session.
  5. When the frontend needs to call an external API, it passes the encrypted cookie to the backend together with the URL and data to invoke the API.
  6. The backend retrieves the access token from the cache and makes a call to the external API including that token on the authorization header.
  7. When the external API returns a response to the backend, this one forwards that response back to the frontend.

Backend For FrontEnd in ASP.NET Core

Visual Studio ships with three templates for SPAs with an ASP.NET Core backend. As shown in the following picture, those templates are ASP.NET Core with Angular, ASP.NET Core with React.js, and ASP.NET Core with React.js and Redux, which includes all the necessary plumbing for using Redux.

As part of this article, we will be discussing how to implement this pattern with the ASP.NET Core with React.js template.

You can use this GitHub repository as a reference for the project you are about to build.

The structure of the project

Projects created with that template from Visual Studio will have the following folder structure.

  • ClientApp, this folder contains a sample SPA implemented with React.js. This is the app that we will modify to support the BFF pattern.
  • Controllers, this folder contains the controllers implemented with ASP.NET Core for the API consumed from the SPA. In other words, it's the backend.
  • Pages, this folder contains server-side pages, which are mostly used for rendering errors on the backend.
  • Startups.cs, this is the file containing the main class where the ASP.NET Core middleware classes are configured as well as the dependency injection container.

Before modifying any code, we will proceed to configure first our application in Auth0. That configuration will give us access to the keys and authentication endpoints for the OpenID middleware in .NET Core.

Auth0 Configuration

To start, you need to access your Auth0 Dashboard. If you don't have an Auth0 account, you can sign up for a free one right now!

Create an application in the Auth0 Dashboard

The first thing we will do is to create a new brand application in the Auth0 Dashboard. An Auth0 application is an entry point for getting the keys and endpoints we will need in our web application. Go to your dashboard, click on the Applications menu on the left, and then Create Application.

The Create Application button will start a wizard to define the configuration of our application. Pick a name for your web application, and select the option Regular Web Applications. Do not confuse your application with a Single Page Web Application. Even if we are going to implement a SPA with React, we will rely on the .NET Core backend to negotiate the ID tokens. When choosing Regular Web Applications, we are telling Auth0 that our application will use the Authorization Code Flow, which requires a backend channel to receive the ID token for OpenID Connect, and that is exactly what we need to get that magic happening in our ASP.NET Core backend.

Once the application is created, go to the Settings tab and take note of the following settings:

  • Domain
  • Client ID
  • Client Secret

Those are the ones you will need to configure the OpenID middleware in the web application.

Configure the Callback URL

The next thing is to configure the Callback URL for our web application. This is the URL where Auth0 will post the authorization code and ID token for OpenID Connect. This URL can be added in the Allowed URLs field for our application. For our sample, we will use https://localhost:5001/callback. If you are planning to deploy the application to a different URL, you will also need to ensure it is listed here.

Configure the Logout URL

The logout URL is where Auth0 will redirect the user after the logout process has been completed. Our web application will pass this URL to Auth0 as part of the returnTo query string parameter. The logout URL for your app must be added to the Allowed Logout URLs field under the application settings, or Auth0 will return an error otherwise when the user tries to do a logout. For our sample, we will use https://localhost:5001.

Create an API in the Auth0 Dashboard

We also need to create an Auth0 API in the Auth0 Dashboard. So, go to the APIs section and click on Create API, as shown in the following picture:

This will open a new window for configuring the API. Configure the following fields under the settings tab in that window.

  • Name, a friendly name or description for the API. Enter Weather Forecast API for this sample.
  • Identifier or Audience, which is an identifier that the client application uses to request access tokens for the API. Enter the string https://weatherforecast.

Under the permissions tab, add a new permission read:weather with the description It allows getting the weather forecast. This is the scope that Auth0 will inject in the access token if the user approves it in the consent screen.

Finally, click on the Save button to save the changes. At this point, our API is ready to be used from .NET Core.

Configuring the ASP.NET Core Application

Our application will use two middleware:

  • The OpenID Connect middleware for handling all the authentication handshake with Auth0.
  • The Authentication Cookie middleware for persisting the authentication session in a cookie also sharing it with the frontend running React.

Open the Package Manager Console for NuGet in Visual Studio and run the following command:

Install-Package Microsoft.AspNetCore.Authentication.Cookies
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect

Once the Nuget packages are installed in our project, we can go ahead and configure the middleware in the Startup.cs class under the root folder of the ASP.NET Core project.

Modify the ConfigureServices method in that class to include the following code.

// BFF/Startup.cs

// ...existing code...

public void ConfigureServices(IServiceCollection services)
{
  services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(o =>
    {
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        o.Cookie.SameSite = SameSiteMode.Strict;
        o.Cookie.HttpOnly = true;
    })
    .AddOpenIdConnect("Auth0", options => ConfigureOpenIdConnect(options));
  
    services.AddHttpClient();

   // ...existing code...
 }

private void ConfigureOpenIdConnect(OpenIdConnectOptions options)
{
    // Set the authority to your Auth0 domain
    options.Authority = $"https://{Configuration["Auth0:Domain"]}";

    // Configure the Auth0 Client ID and Client Secret
    options.ClientId = Configuration["Auth0:ClientId"];
    options.ClientSecret = Configuration["Auth0:ClientSecret"];

    // Set response type to code
    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;

    options.ResponseMode = OpenIdConnectResponseMode.FormPost;

    // Configure the scope
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("offline_access");
    options.Scope.Add("read:weather");
    
    // Set the callback path, so Auth0 will call back to http://localhost:3000/callback
    // Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
    options.CallbackPath = new PathString("/callback");

    // Configure the Claims Issuer to be Auth0
    options.ClaimsIssuer = "Auth0";

    // This saves the tokens in the session cookie
    options.SaveTokens = true;
    
    options.Events = new OpenIdConnectEvents
    {
        // handle the logout redirection
        OnRedirectToIdentityProviderForSignOut = (context) =>
        {
            var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";

            var postLogoutUri = context.Properties.RedirectUri;
            if (!string.IsNullOrEmpty(postLogoutUri))
            {
                if (postLogoutUri.StartsWith("/"))
                {
                    // transform to absolute
                    var request = context.Request;
                    postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
                }
                logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
            }
            context.Response.Redirect(logoutUri);
            context.HandleResponse();

            return Task.CompletedTask;
        },
        OnRedirectToIdentityProvider = context => {
            context.ProtocolMessage.SetParameter("audience", Configuration["Auth0:ApiAudience"]);
            return Task.CompletedTask;
        }
    };
}

// ...existing code...

This code configures the OpenID Connect middleware to point to Auth0 for authentication and the Cookie middleware for persisting the authentication session in cookies. Let's discuss different parts of this code more in detail so you can understand what it does.

services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(o =>
    {
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        o.Cookie.SameSite = SameSiteMode.Strict;
        o.Cookie.HttpOnly = true;
    })

It configures authentication to rely on the session cookie as the primary authentication mechanism if no other is specified in one of the web application's controllers. It also injects the cookie middleware with a few settings that restrict how the cookie can be used on the browsers. In our case, the cookie can only be used under HTTPS (CookieSecurePolicy.Always), it's not available on the client side (HttpOnly = true), and uses a site policy equals to strict (SameSiteMode.Strict). This last one implies the cookie will only be sent if the domain for the cookie matches exactly the domain in the browser's URL. All these settings help to prevent potential attacks with scripting on the client side.

options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.ResponseMode = OpenIdConnectResponseMode.FormPost;

The OpenID Connect middleware is configured to use ResponseType equals to CodeIdToken (Hybrid flow), which means our web application will receive an authorization code and ID token directly from the authorization endpoint right after the user is authenticated. We will use the authorization code in exchange for an access token for calling a backend API hosted on a different site.

// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("offline_access");
options.Scope.Add("read:weather");

The openid scope is required as part of the OpenID Connect authentication flow. The offline_access is for requesting a refresh token and read:weather is specific to the API we will call later as part of this sample.

options.SaveTokens = true;

The SaveTokens option tells the OpenID Connect middleware that all the tokens (id token, refresh token, and access token) received from the authorization endpoint during the initial handshake must be persisted for later use. By default, the middleware persists those tokens in the encrypted session cookie, and we will use that for our sample.

OnRedirectToIdentityProvider = context => {
    context.ProtocolMessage.SetParameter("audience", Configuration["Auth0:ApiAudience"]);
    return Task.CompletedTask;
},

The OpenID Connect middleware does not have any property to configure the audience parameter that Auth0 requires for returning an authorization code for an API. We are attaching some code to the OnRedirectToIdentityProvider event for setting that parameter before the user is redirected to Auth0 for authentication.

services.AddHttpClient();

The extension method AddHttpClient injects an IHttpClientFactory with default settings to create instances of the class HttpClient. We will use it to make calls to the external API.

The next step is to modify the Configure method to tell ASP.NET Core that we want to use the authentication and authorization middleware. Those middleware will integrate automatically with the authentication session cookies.

Insert the following code as it is shown below:

// Startup.cs

// ...existing code...

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...existing code...
    app.UseRouting();

    // Code goes here
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller}/{action=Index}/{id?}");
     });
  
    // ...existing code...
}

Update the existing appSettings.json file and include the settings we got from the Auth0 Dashboard before. Those are Domain, Client ID, Client Secret, and ApiAudience.

{
  "Logging": {
      "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
      }
    },
  "AllowedHosts": "*",
  "Auth0": {
    "Domain": "<domain>",
    "ClientId": "<client id>",
    "ClientSecret": "<client secret>",
    "ApiAudience": "https://weatherforecast"
  }
}

Add the ASP.NET Core Controllers for handling authentication

Create a new ASP.NET controller in the Controllers folder and call it AuthController. This controller has three actions.

  • Login for initiating the OpenID Connect login handshake with Auth0.
  • Logout for logging out from the web application and also from Auth0.
  • GetUser for getting data about the authenticated user in the current session. This is an API that the React application will invoke to get the authentication context for the user.

This is the code for the Login action:

// BFF/Controllers/AuthController.cs

// ...existing code...

public ActionResult Login(string returnUrl = "/")
{
  return new ChallengeResult("Auth0", new AuthenticationProperties() 
    { 
      RedirectUri = returnUrl
    }
  );
}

// ...existing code...

It is an action that returns a ChallengeResult with the authentication schema to be used. In this case, it is Auth0, which is the schema we associated with our OpenID Connect middleware in the Startup class. This result is a built-in class shipped with ASP.NET Core to initiate an authentication handshake from the authentication middleware.

The logout action looks as follows:

// BFF/Controllers/AuthController.cs

// ...existing code...

[Authorize]
public async Task<ActionResult> Logout()
{
    await HttpContext.SignOutAsync();

    return new SignOutResult("Auth0", new AuthenticationProperties
    {
        RedirectUri = Url.Action("Index", "Home")
    });
}

// ...existing code...

It returns a SignOutResult that will log the user out of the application and also initiate the sign-out process with Auth0. As it happened with the ChallengeResult, this SignOutResult is also a built-in result that the authentication middleware will process. We also decorated the action with the [Authorize] attribute as it should only be invoked if the user is authenticated.

Finally, the GetUser API code is the following:

// BFF/Controllers/AuthController.cs

// ...existing code...

public ActionResult GetUser()
{
  if (User.Identity.IsAuthenticated)
  {
    var claims = ((ClaimsIdentity)this.User.Identity).Claims.Select(c =>
                    new { type = c.Type, value = c.Value })
                    .ToArray();

    return Json(new { isAuthenticated = true, claims = claims });
 }

 return Json(new { isAuthenticated = false });
}

// ...existing code...

If the user is authenticated, it returns the user identity as a set of claims serialized as JSON. Otherwise, it just returns a flag indicating the user is not authenticated.

Require authentication in other controllers

The WeatherForecast controller included in the template allows anonymous calls. To make it more interesting in our sample, we will convert it to require authenticated calls. Fortunately, that is as simple as adding a top-level Authorize attribute in the class definition.

// BFF/Controllers/WeatherForecastController.cs

// ...existing code...

[ApiController]
[Authorize]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{

// ...existing code...

Negotiate an Access Token and call a remote API

We will convert the WeatherForecast controller in our web application to act as a reverse proxy and call the equivalent API hosted remotely on a different site. This API will require an access token, so the controller will have to negotiate first the authorization code that is persisted in the session cookie.

 public WeatherForecastController(
            IHttpClientFactory httpClientFactory,
            IConfiguration configuration)
{
    _httpClientFactory = httpClientFactory;

    if (configuration["WeatherApiEndpoint"] == null)
        throw new ArgumentNullException("The Weather Api Endpoint is missing from the configuration");

    _apiEndpoint = new Uri(configuration["WeatherApiEndpoint"], UriKind.Absolute);
}

The constructor on this controller receives an instance of an IHttpClientFactory that we previously registered in the Startup.cs file for creating HttpClient instances and an instance of IConfiguration to retrieve settings from the configuration file. The endpoint for the Weather API is retrieved from the configuration using the WeatherApiEndpoint key. That key in the appSettings.json only references the URL for the remote API as it is shown below:

// appSettings.json
{
  // ... other settings ...
  "WeatherApiEndpoint": "https://localhost:44385/"
}

The following code shows the implementation of the Get method. This is the actual remote API invoked by passing the expected authorization headers:

// BFF/Controllers/WeatherForecastController.cs

// ...existing code...

[HttpGet]
public async Task Get()
{
    var accessToken = await HttpContext.GetTokenAsync("Auth0", "access_token");

    var httpClient = _httpClientFactory.CreateClient();

    var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_apiEndpoint, "WeatherForecast"));
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    var response = await httpClient.SendAsync(request);

    response.EnsureSuccessStatusCode();

    await response.Content.CopyToAsync(HttpContext.Response.Body);
}

// ...existing code...

The trick for getting the access token is in the following line,

var accessToken = await HttpContext.GetTokenAsync("Auth0", "access_token");

GetTokenAsync is an extension method available as part of the authentication middleware in ASP.NET Core. The first argument specifies the authentication schema to be used to get the token, which is our OpenID Connect middleware configured with the name "Auth0". The second argument is the token to be used. In the case of OpenID Connect, the possible values are "access_token" or "id_token". If the access token is not available or already expired, the middleware will use the refresh token and authorization code to get one. Since our middleware was pointing to the WeatherForecast API with the audience attribute and the scope we previously configured, Auth0 will return an access token for that API.

var httpClient = _httpClientFactory.CreateClient();

var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_apiEndpoint, "WeatherForecast"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

var response = await httpClient.SendAsync(request);

response.EnsureSuccessStatusCode();

await response.Content.CopyToAsync(HttpContext.Response.Body);

The code above forwards the request to the remote API using a new instance of HttpClient created with the IHttpClientFactory injected in the constructor. The access token is passed as a Bearer token in the authorization header.

Configuring the Remote API

As the remote API, we will use the one provided with Visual Studio's ASP.NET Web API template that returns the weather forecast data.

Create the ASP.NET Core API in Visual Studio

Visual Studio ships with a single template for .NET Core APIs. That is ASP.NET Core Web API, as it is shown in the image below.

The structure of the project

Projects created with that template from Visual Studio will have the following structure:

  • Controllers, this folder contains the controllers for the API implementation.
  • Startup.cs, this is the main class where the ASP.NET Core middleware classes and the dependency injection container are configured.

Configuring the project

Our application will only use the middleware for supporting authentication with JWT as bearer tokens.

Open the Package Manager Console for NuGet in Visual Studio and run the following command:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

Once the NuGet packages are installed in our project, we can go ahead and configure them in the Startup.cs class file.

Modify the ConfigureServices method in that class to include the following code:

// Api/Startup.cs

// ...existing code...

public void ConfigureServices(IServiceCollection services)
{
    var authentication = services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer("Bearer", c =>
        {
        c.Authority = $"https://{Configuration["Auth0:Domain"]}";
        c.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidAudiences = Configuration["Auth0:Audience"].Split(";"),
            ValidateIssuer = true,
            ValidIssuer = $"https://{Configuration["Auth0:Domain"]}";
        };
    });

    services.AddControllers();
            
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
    });

    services.AddAuthorization(o =>
    {
        o.AddPolicy("read:weather", p => p.
            RequireAuthenticatedUser().
            RequireScope("read:weather"));
    });
}

// ...existing code...

This code performs two things. It configures the JWT middleware to accept access tokens issued by Auth0 and defines an authorization policy for checking the scope set on the token. The policy checks for a claim or attribute called scope with a value read:weather, which is the scope we previously configured for our API in the Auth0 dashboard. RequireScope is a custom extension we will write as part of this sample to check for the scope present in the JWT access token.

The next step is to modify the Configure method to tell ASP.NET Core that we want to use the authentication and authorization middleware. That middleware will integrate automatically with the authentication session cookies.

Insert the new code as shown below in the Startup.cs file:

// Api/Startup.cs

// ...existing code...

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  // ...existing code...
  
  app.UseRouting();
            
  app.UseAuthentication();
  app.UseAuthorization();

  app.UseEndpoints(endpoints =>
  {
    endpoints.MapControllers();
  });
}

// ...existing code...

Update the existing appSettings.json file and include the settings we got from the Auth0 dashboard before. Those are Domain and API's Audience.

{
  "Logging": {
      "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
      }
    },
  "AllowedHosts": "*",
  "Auth0": {
    "Domain": "<domain>",
    "Audience": "https://weatherforecast"
  }
}

RequireScope policy

ASP.NET Core does not include any policy out of the box for checking an individual scope in a JWT access token. To overcome this shortcoming, we will create a custom policy. For this purpose, create a new Authorization folder. Then add three new files on it, ScopeHandler.cs, ScopeRequirement.cs, and AuthorizationPolicyBuilderExtensions.cs. We will discuss the purpose of each one next.

Add a new file ScopeHandler.cs to the Authorization folder with the following content:

// Api/Authorization/ScopeHandler.cs

using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Api.Authorization
{
    public class ScopeHandler :
             AuthorizationHandler<ScopeRequirement>
    {
        protected override Task HandleRequirementAsync(
          AuthorizationHandlerContext context,
          ScopeRequirement requirement)
        {
            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var success = context.User.Claims.Any(c => c.Type == "scope" && 
                c.Value.Contains(requirement.Scope));

            if (success)
                context.Succeed(requirement);
            
            return Task.CompletedTask;
        }
    }
}

The authentication middleware parses the JWT access token and converts each attribute in the token as a claim attached to the current user in context. Our policy handler uses the claim associated with the scope for checking that the expected scope is there (read:weather).

Every implementation of AuthorizationHandler must be associated with an implementation of IAuthorizationRequirement that describes the authorization requirements for the handler. In our case, the implementation looks as it is described in the following.

Add the following content in the ScopeRequirement.cs file,

// Api/Authorization/ScopeRequirement.cs

using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Api.Authorization
{
    public class ScopeRequirement : IAuthorizationRequirement
    {
        public string Scope { get; private set; }

        public ScopeRequirement(string scope)
        {
            Scope = scope;
        }
    }
}

It's a very simple implementation that describes a requirement in terms of a scope. That's the expected scope in the JWT Access Token.

Finally, the class AuthorizationPolicyBuilderExtensions.cs includes the RequireScope extension method for injecting the ScopeHandler instance in the Startup.cs class when the policy is configured.

// Api/Authorization/AuthorizationPolicyBuilderExtensions.cs

using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Api.Authorization
{
    public static class AuthorizationPolicyBuilderExtensions
    {
        public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, string scope)
        {
            return builder.AddRequirements(new ScopeRequirement(scope));
        }
    }
}

Require authentication in the API controller

The WeatherForecast controller included in the template allows anonymous calls. We will convert it to require authenticated calls using the Authorize attribute. That attribute will also reference the policy we previously defined in the Startup.cs file.

// Api/Controllers/WeatherForecastController.cs

// ...existing code...

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
  [HttpGet]
  [Authorize("read:weather")]
  public IEnumerable<WeatherForecast> Get()
  {
    
// ...existing code...

This attribute will do two things,

  • It will activate the authorization middleware that will check if the call was authenticated and there is one user identity set in the current execution context.
  • It will run the read:weather policy to make sure the user identity contains the required permissions. In our case, it will check the access token includes a scope called read:weather.

Once we run this project in Visual Studio, the API will only accept authenticated calls with access tokens coming from Auth0.

Securing the React Application

So far, we have added all the plumbing code on the backend to enable authentication with Auth0 using OpenID Connect. The backend handles user authentication and configures a cookie that we can share with the React app. We also added a GetUser API that can be used to determine whether the user is authenticated and get basic identity information about them. Let's now see the needed changes for the React client application.

React Context for Authentication

As authentication is a core concern that we will use across all the components in the React application, it makes sense to make it available as a global context using the context pattern. Move into BFF/ClientApp/src folder and create a context folder. Then add a file AuthContext.js to the newly created folder. Paste the following code on the file:

// BFF/ClientApp/src/context/AuthContext.js

import React, { useState, useEffect, useContext } from "react";

export const AuthContext = React.createContext();
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({
    children
}) => {
    const [isAuthenticated, setIsAuthenticated] = useState();
    const [user, setUser] = useState();
    const [isLoading, setIsLoading] = useState(false);

    const getUser = async () => {
        const response = await fetch('/auth/getUser');
        const json = await response.json();

        setIsAuthenticated(json.isAuthenticated);
        setIsLoading(false);
        if (json.isAuthenticated) setUser(json.claims);
    }

    useEffect(() => {
        getUser();
    }, []);

    const login = () => {
        window.location.href = '/auth/login';
    }

    const logout = () => {
        window.location.href = '/auth/logout';
    }

    return (
        <AuthContext.Provider
            value={{
                isAuthenticated,
                user,
                isLoading,
                login,
                logout
            }}
        >
            {children}
        </AuthContext.Provider>
    );
};

This context object provides methods for starting the login and logout handshake with the backend and getting the authenticated user.

Modify the index.js file to reference this context provider.

// BFF/ClientApp/src/index.js

// ...existing code...

ReactDOM.render(
    <AuthProvider>
        <BrowserRouter basename={baseUrl}>
            <App />
        </BrowserRouter>
    </AuthProvider>,
    rootElement);

// ...existing code...

Add the login and logout routes

The React Router configuration uses the authentication context to redirect the user to login and logout URLs on the backend. It also forces the user authentication for routes that are protected, such as the one for fetching the weather data.

To add these protected routes, modify the App.js file to include this code:

// BFF/ClientApp/src/App.js

import { Redirect, Route } from 'react-router';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { FetchData } from './components/FetchData';
import { useAuth } from './context/AuthContext';

import './custom.css'

const App = () => {

    const { isAuthenticated, login, logout } = useAuth();

    return (
        <Layout>
            <Route exact path='/' component={Home} />
            <Route path='/fetch-data' component={isAuthenticated ? () => { return <FetchData /> } : () => { login(); return null; }}/>
            <Route path='/login' component={() => { login(); return null }} />
            <Route path='/logout' component={() => { logout(); return null }}></Route>
        </Layout>
    );
}

export default App;

The fetch-data route, for example, checks if the user is authenticated before returning the FetchData component. If the user is not authenticated, it calls the login function in the authentication context, which ultimately redirects the user to the Login endpoint in the backend.

Modify the application menu

Another very common feature in web applications is to make menu options visible or not, depending on the user authentication status. As we did in the React Router, the same thing can be accomplished for the menu options using the isAuthenticated function from the authentication context.

So, move to the ClientApp/src/components folder. Then modify the NavMenu.js file to check the authentication status as it is shown below.

// BFF/ClientApp/src/components/NavMenu.js

// ...existing code...

return (
        <header>
            <Navbar className="navbar-expand-sm navbar-toggleable-sm ng-white border-bottom box-shadow mb-3" light>
                <Container>
                    <NavbarBrand tag={Link} to="/">Auth0 Backend For FrontEnd Authentication</NavbarBrand>
                    <NavbarToggler onClick={toggleNavbar} className="mr-2" />
                    <Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={!collapsed} navbar>
                        <ul className="navbar-nav flex-grow">
                            <NavItem>
                                <NavLink tag={Link} className="text-dark" to="/">Home</NavLink>
                            </NavItem>
                            <NavItem>
                                <NavLink tag={Link} className="text-dark" to="/fetch-data">Fetch data</NavLink>
                            </NavItem>
                            <NavItem>
                            {!isAuthenticated && <NavItem>
                                <NavLink tag={Link} className="text-dark" to="/login">Login</NavLink>
                            </NavItem>}
                            {isAuthenticated && <NavItem>
                                <NavLink tag={Link} className="text-dark" to="/logout">Logout</NavLink>
                            </NavItem>}
                        </ul>
                    </Collapse>
                </Container>
            </Navbar>
        </header>
 );
                             
// ...existing code...

Add a component to show user data

The authentication context provides a getUser function in case you want to show the user's basic data coming from Auth0 on the React application. That function returns a collection of claims about the user's identity coming from the backend API GetUser.

The following code shows a component that enumerates those claims.

// BFF/ClientApp/src/components/User.js

import React, { useEffect, useState } from 'react';
import { useAuth } from '../context/AuthContext';

export const User = () => {

    const { user } = useAuth();

    const renderClaimsTable = function (claims) {
        return (
            <table className='table table-striped' aria-labelledby="tabelLabel">
                <thead>
                    <tr>
                        <th>Type</th>
                        <th>Value</th>
                    </tr>
                </thead>
                <tbody>
                    {claims.map(claim =>
                        <tr key={claim.type}>
                            <td>{claim.type}</td>
                            <td>{claim.value}</td>
                        </tr>
                    )}
                </tbody>
            </table>
        );
    }

    return (
        <div>
            <h1 id="tabelLabel" >User claims</h1>
            <p>This component demonstrates fetching user identity claims from the server.</p>
            {renderClaimsTable(user)}
        </div>
    );

}

Run the Web Application

From Visual Studio, click on the Run button but select your project name from the dropdown option instead of "IIS Express". That will run the application using the Kestrel, the built-in web server included in .NET Core. Kestrel runs on "https://localhost:5001", which is the URL we previously configured in Auth0.

About the Login Flow

Here is what happens when the user authenticates with the application we have built:

  • The user clicks on the Log In button and is directed to the Login route.
  • The ChallengeResult response tells the ASP.NET authentication middleware to issue a challenge to the authentication handler registered with the Auth0 authentication scheme parameter. The parameter uses the "Auth0" value you passed in the call to AddOpenIdConnect in the Startup class.
  • The OIDC handler redirects the user to the Auth0's /authorize endpoint, which displays the Universal Login page. The user can log in with their username and password, social provider, or any other identity provider.
  • Once the user has logged in, Auth0 calls back to the /callback endpoint in your application and passes along an authorization code.
  • The OIDC handler intercepts requests made to the /callback path.
  • The handler looks for the authorization code, which Auth0 sent in the query string.
  • The OIDC handler calls the Auth0's /oauth/token endpoint to exchange the authorization code for the user's ID and access token.
  • The OIDC middleware extracts the user information from the claims in the ID token.
  • The OIDC middleware returns a successful authentication response and sets a cookie that indicates that the user is authenticated. The cookie contains the claims with the user's information. The cookie is stored so that the cookie middleware will automatically authenticate the user on any future requests. The OIDC middleware receives no more requests unless it is explicitly challenged.
  • The React application uses the authentication context to issue an API call to the GetUser API. This API returns the user claims from the authentication cookie.
  • The React application renders the UI Component using the authenticated user's identity.

Conclusion

The BFF pattern is an ideal solution for authentication if you can afford to pay extra money for a dedicated backend. It will help you avoid headaches when dealing with access tokens and how to keep them safe on your client-side application. The backend will do all the heavy lifting, so that you can focus only on UI/UX concerns in the frontend.

You can download from this GitHub repository the full source code of the sample project built in this article.