This tutorial was written by Sesethu Mhlana, a software engineer with a passion for developing innovative solutions for web and desktop applications. Connect with him on LinkedIn to see more of his work!


User authentication and access control in React applications ensure that only authorized users can access and interact with protected resources. Magic link authentication is a type of user authentication where the user receives a unique link via email to log in to an application. This automatically authenticates and grants the user access without needing to remember or enter a password, simplifying the login process. Role-based access control (RBAC) regulates access to a system based on the roles of individual users within an organization—meaning users gain permissions based on their roles rather than individually granted permissions.

Descope is a developer-friendly tool that offers no / low code workflows that you can leverage to easily integrate magic links and RBAC into your React application. Descope provides a streamlined authentication and user management platform for web and mobile apps, emphasizing security with built-in measures against threats like token reuse and suspicious logins.

This article will guide you through seamlessly integrating Descope into your React application for authentication and authorization. You’ll set up passwordless logins using a magic link authentication flow and implement secure access control effortlessly with few lines of code.

As mentioned, magic link authentication provides a secure and user-friendly login method by allowing users to log in via a unique, time-sensitive link sent to their email or phone. The process begins when the user requests a login by entering their email address on the application. The server then generates a unique token and sends it to the user’s email as a magic link. When the user clicks this link, it directs them back to the application, where the server validates the token. If valid, the user is authenticated and logged in without needing a password, ensuring security and convenience by relying on the user’s email for verification.

How magic links work
Fig: How magic link authentication works

This method is quick and intuitive, eliminates the need for the user to remember or manage passwords, and minimizes common password-related security risks, such as theft and reuse. As they are single-use and time-sensitive, magic links limit the risk of unauthorized access if intercepted, and relying on the user’s email for verification offers robust protection against phishing attacks, ensuring that only legitimate users with access to their email can authenticate.

In contrast, traditional authentication requires users to create and remember complex passwords, which can be cumbersome and prone to security vulnerabilities, such as brute-force attacks and phishing. This means you need additional security measures like multi-factor authentication (MFA). Please note that MFA can also be used with magic links for an additional security layer.

Implementing magic link authentication and RBAC in a React application involves setting up a mechanism where users can log in using magic links sent to their email addresses and then controlling access to different parts of the application based on the roles assigned to them. You will be creating a recipe app that will fetch recipes from DummyJSON and display them to logged-in users. You’ll then use RBAC to render certain information about the recipes to the user based on the role assigned to them.

To explore the Descope authentication and user management features, you need to create a Descope account. Descope offers a Free Forever account, allowing users to build and test their applications free of charge before taking them to production. This is the account you’ll be using later in the tutorial.

Creating the React application

To begin, you need to create a React app and install the necessary Descope package. Create the application using the create-react-app command:

npx create-react-app descope-app

After the project is created, install the Descope package using the following command:

npm i @descope/react-sdk

Configuring an authentication flow on Descope

To integrate Descope into your project, you need to configure an authentication flow using the developer console. Sign in to Descope, and this takes you to the console. Then click Getting Started and choose Consumers:

React magic link blog onboarding wizard 1
Fig: Descope Getting Started Wizard

Click Next and choose Magic Link as the form of authentication:

React magic link blog onboarding wizard 2
Fig: Choosing magic link authentication

In the next step, you can choose Go ahead without MFA as you won’t be using it for this tutorial. You can always set up MFA later. Then, you’ll see a preview of the selected options:

React magic link blog onboarding wizard 3
Fig: Previewing selected options

To integrate the authentication flow into your project, you first have to wrap the entire application with <AuthProvider>. To do this, replace the code in index.js with the following code to set up the SDK and wrap your whole application in its context (get the project ID from the Project tab in the developer console):

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from '@descope/react-sdk';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
        <AuthProvider
            projectId='<your-descope-project-id>'
        >
            <App />
        </AuthProvider>
    </React.StrictMode>
);

You now have easy access to the logged-in user via the user object.

Now that the authentication flow has been integrated into the application, replace the code in App.js with the following code to configure the application to use the Descope authentication flow:

import './App.css';
import { useDescope, useSession, useUser } from '@descope/react-sdk'
import { Descope } from '@descope/react-sdk'
import { React, useEffect, useState } from 'react';

function App() {
  const { isAuthenticated, isSessionLoading } = useSession()
  const { user, isUserLoading } = useUser()
  const { logout } = useDescope()

  // State property to store the recipes
  const [recipes, setRecipe] = useState([]);

  // Fetch the recipes and store them in the recipe state property
  const fetchRecipes = async () => {
    await fetch("https://dummyjson.com/recipes")
        .then((response) => response.json())
        .then((results) => {
          setRecipe(results.recipes);
        })
        .catch(error => console.error(error));
  }
  
  useEffect(() => {
    fetchRecipes();
  }, []);

  const handleLogout = () => {
    logout()
  };

  return <>
    // Log a message if the authentication succeeds or fails
    {!isAuthenticated &&
      (
        <div style={{ display: "flex", justifyContent: "center" }}>
          <div style={{ width: "400px" }}>
            <Descope
              flowId="sign-up-or-in"
              onSuccess={(e) => console.log(e.detail.user)}
              onError={(e) => console.log('Could not log in!')}
            />
          </div>
        </div>
      )

    }

    {
      (isSessionLoading || isUserLoading) && <p>Loading...</p>
    }

    {!isUserLoading && isAuthenticated &&
      (
        <div style={{padding:"0 0 3rem 3rem"}}>
          <p>Hello {user.name}</p>
          <button onClick={handleLogout}>Logout</button>
          <div style={{padding:"1rem 0 0 0"}}>Here are some delicious recipes:</div>
          {recipes.length > 0 && 
            <>
              {recipes.map((recipe) => {
                return (
                  <div key={recipe.id}>
                    <h1>{recipe.name}</h1>
                    <p>Ingredients: {recipe.ingredients.join(', ')}.</p>
                    {recipe.instructions.map((instruction, index) => {
                      return (<p key={index}>{instruction}</p>)
                    })}
                  </div>
                )
              })}
            </>
          }
        </div>
      )}
  </>;
}
export default App;

When the user logs in, the application fetches recipes from the DummyJSON API and displays them. The information about the logged-in user is stored in the user object, and from this object, you are able to get details like the user’s name.

Now, you can run this application. Navigate to your project directory using the following command:

cd descope-app

Run the application using this command:

npm start

After successfully compiling, navigate to the URL shown in the terminal. It is usually http://localhost:3000/. This displays the login screen:

React magic link blog login screen
Fig: Login screen

Enter the email address you want to authenticate with and click on Continue. You will receive an email with the link. Click on the link, and you will be authenticated and redirected to the application. Now that you’re authenticated, you will be able to see the recipes:

React magic link blog after login
Fig: After authentication is successful

Implementing RBAC with Descope

Now that the user can see the recipes, you can configure RBAC to restrict or show some of the data to specific users.

RBAC is a method of regulating access to resources by assigning users to roles, which are defined by specific permissions related to job functions. This simplifies user management and enhances security by adhering to the principle of least privilege. You can learn more in Descope’s RBAC documentation.

Defining user roles

To implement RBAC in your React application, you initially need to establish the necessary user roles in Descope. For this example, you’ll define a chef role and a user role. The user will only be permitted to list the recipes, and the chef will be permitted to list the recipes and view the ingredients and instructions.

In the Descope console, navigate to the Authorization page. Add a new role by clicking the + Role button and fill in the following details:

  • Name: Chef

  • Description: A chef who will view all recipes, including the ingredients and instructions.

React magic link blog chef role
Fig: Adding a role

For this tutorial, leave the permissions as they are.

Then add another role for the user:

  • Name: User

  • Description: A user who will list the recipes.

You should now have two roles created:

React magic link blog roles
Fig: Viewing newly added roles

To assign a role to a user, navigate to Users, locate the user you would like to assign a role to, and select Edit from the menu on the right:

React magic link blog roles editing
Fig: Editing a user’s details to assign a role

In the modal that appears, locate the Authorization section and select the Chef role in the Roles drop-down:

React magic link blog roles assigning
Fig: Assigning a role to a user

Click Save. You have now assigned a role to the user.

Implementing roles in your React app

To use these new roles, you need to make a few changes to the code. Create a new file called roles.js and add the following code to define the roles to be used in the application:

export const roles = { 
    USER: 'User', 
    CHEF: 'Chef' 
};

Then, replace App.js with the following code to conditionally render certain details about the recipes based on the roles assigned to the logged-in user:

import './App.css';
import { useDescope, useSession, useUser } from '@descope/react-sdk'
import { Descope } from '@descope/react-sdk'
import { React, useEffect, useState } from 'react';
import { roles } from './roles';

function App() {
  const { isAuthenticated, isSessionLoading } = useSession()
  const { user, isUserLoading } = useUser()
  const { logout } = useDescope()

  const [recipes, setRecipe] = useState([]);

  const fetchRecipes = async () => {
    await fetch("https://dummyjson.com/recipes")
        .then((response) => response.json())
        .then((results) => {
          setRecipe(results.recipes);
        })
        .catch(error => console.error(error));
  }
  
  useEffect(() => {
    fetchRecipes();
  }, [isAuthenticated, user]);

  const handleLogout = () => {
    logout()
  };

  return <>
    {!isAuthenticated &&
      (
        <Descope
          flowId="sign-up-or-in"
          onSuccess={(e) => console.log(e.detail.user)}
          onError={(e) => console.log('Could not log in!')}
        />
      )
    }

    {
      (isSessionLoading || isUserLoading) && <p>Loading...</p>
    }

    {!isUserLoading && isAuthenticated &&
      (
        <div style={{padding:"0 0 3rem 3rem"}}>
          <p>Hello {user.name}</p>
          <button onClick={handleLogout}>Logout</button>
          {(user.roleNames.includes(roles.CHEF) || user.roleNames.includes(roles.USER)) && 
            <div>
              <div style={{padding:"1rem 0 0 0"}}>Here are some delicious recipes:</div>

              {recipes.length > 0 && 
                <>
                  {recipes.map((recipe) => {
                    return (
                      <div key={recipe.id}>
                        <h1>{recipe.name}</h1>

                        {
                          user.roleNames.includes(roles.CHEF) &&
                          <>
                            <p>Ingredients: {recipe.ingredients.join(', ')}.</p>
                            {recipe.instructions.map((instruction, index) => {
                              return (<p key={index}>{instruction}</p>)
                            })}
                          </>
                        }
                      </div>
                    )
                  })}
                </>
              }
            </div>
            }
        </div>
      )}
  </>;
}
export default App;

The preceding code gets the roles for the user from user.roleNames, which is an array that contains all the roles assigned to the user, and uses this information to determine what to render.

The recipe titles are rendered for both the chef and user roles, but the ingredients and instructions are rendered only for the chef role. For the user assigned the chef role, you see the recipe titles together with the ingredients and instructions.

Descope provides other alternatives to magic links:

Enchanted links

An enchanted link is an enhanced version of a magic link. It is a unique, single-use link that is sent to the user via email for authentication purposes. It allows a user to initiate the login process on one device and then authenticate by clicking the enchanted link on another device.

The receiving device requires the user to select the correct link from a set of three links sent to them. Once the correct link is clicked, the user’s session on the initial device is validated, and they are logged in. Enchanted links can be used to sign up a new user or to sign in an existing user.

Descope Enchanted Links
Fig: Descope enchanted links

You can learn more about customizing enchanted links with Descope Flows here.

Embedded links

An embedded link is another authentication mechanism that generates a unique token used to authenticate an already registered user. This token can be distributed to users via different methods, like email or SMS, or it can be used in a machine-to-machine operation similar to an enchanted link.

An embedded link token is authenticated using the magic link verification function. The token is generated using the Descope SDK in a backend such as Node.js and then sent to the user. After the user clicks on it, the token is then sent to the backend for verification using the magic link verification function. If you want to learn how to add embedded links using Descope Flows, you can check out this guide.

Conclusion

In this article, you learned how to implement a magic link authentication flow in your React application. You saw how to create and assign roles to a user and manage what the user has access to. When implemented in your application, magic links can boost user adoption and improve conversion rates by eliminating the challenges associated with password-based authentication. This method also reduces the complexity of implementing authentication for developers because it requires less infrastructure and development effort.

Descope simplifies the addition of magic links through visual workflows that speed up initial setup as well as ongoing modifications to your authentication journeys.

Fig: Drag-and-drop magic links with Descope
Drag-and-drop magic link authentication with Descope

Sign up for a free Descope account to continue your authentication journey. If you have questions about the platform, book time with the Descope team.