Skip to main contentArrow Right

This tutorial was written by Kevin Kimani, a passionate developer and technical writer who enjoys explaining complicated concepts in a simple way. You can check out his GitHub or X account to connect and see more of his work!


User authentication in React Native applications validates the identity of users who interact with your application. This can involve a combination of credentials, such as username and password; biometric data, such as fingerprints or face scans; or social login with providers like Google and GitHub.

Using traditional authentication methods, like password-based authentication, can introduce unwanted complexities in the implementation and security of your React Native application. Building features such as user registration, login, session management, and password resets from scratch usually involve complex logic that can lead to longer development times and increased chances of bugs. These features may also require significant expertise in cryptography and security best practices to ensure the integrity and confidentiality of sensitive user information.

Building authentication for your application in-house can further intensify these complexities for the following reasons:

  • Maintenance overhead: You are responsible for maintaining and updating the authentication codebase as the application’s authentication needs evolve.

  • Security risks: You need a great understanding of security principles and best practices. Without proper expertise, your system can be vulnerable to attacks by malicious actors.

  • Delayed development: You’ll have to divert time and focus away from building your application’s core features. You may end up spending more time resolving authentication-related issues that have already been fixed by existing solutions.

In this article, you will learn how to add user authentication to a React Native application with Descope. Additionally, you will learn how to implement role-based access control (RBAC) and conditionally render in-app data based on the role of the user.

Prerequisites

Before starting, you need the following to complete this tutorial:

  • A React Native development environment set up.

  • Node.js v18 and Yarn installed on your local machine.

  • An emulator with Google Play Store support (indicated by the Google Play icon when creating a new emulator device). This guide uses Pixel 4:

Fig: Selecting hardware

Note: Make sure that you set up a screen lock for your emulator. For this guide, the emulator has a PIN set up.

To demo how to implement RBAC and render in-app data based on the user’s role, this guide uses a simple blogging platform with a couple of posts. Each post has a title, body, and published status, which is a Boolean indicating whether a post has been published or not.

Through Descope, you create two roles: editor and viewer. Editors can access all the posts and toggle the published status. Meanwhile, viewers are able to view only the published posts. For authentication in the app, you will use passkeys / Web Authentication (WebAuthn) as the primary authentication method with an email one-time password (OTP) as a fallback (multi-factor authentication or MFA).

With Descope providing the authentication and user management functionalities, a custom backend will serve the posts to the users based on their roles.

Creating a Descope Project

With your Descope account ready, you can log in to your console and follow the Getting Started guide to create a project.

Under Who uses your application?, choose Consumers as you are building an application that will be used by individuals, and click Next:

Fig: Selecting the target users for your app

For the authentication method, choose Passkeys (WebAuthn) and click Next:

Fig: Primary authentication method

Select One-time Password as the MFA method and click Next:

Fig: Selecting the MFA method

On the next page, select the first login screen option and click Next. You can modify the appearance of the screen later:

Fig: Selecting your OOTB authentication screen

The next screen allows you to preview the generated authentication pages and the flows that are created for your project. Click Next to generate the flows:

Fig: Preview of the configuration

Once the flows are generated, select Project from the sidebar and copy your project ID from the Project page:

Fig: Copying the Project ID

You’ll come back to the Descope console later on to modify the flows. For now, let’s move on to setting up the backend.

Setting up the backend

To keep the focus on the Descope and React Native integration, you can clone the pre-prepared backend code from GitHub to your local machine using this command:

git clone https://github.com/kimanikevin254/descope-react-native-auth.git --branch server-code --single-branch

Here’s a simplified explanation of the files in the server folder that are most relevant to this tutorial:

  • index.js: This file initializes an Express server to handle the requests from the React Native application. It also initializes a Descope client instance (descopeClient) through the Descope SDK for Node.js that interacts with the Descope API to validate the user’s session token. Prisma ORM is also initialized in this file to handle interactions with the SQLite database (dev.db), which is stored in the prisma folder. Two API routes are defined in this file: / and /toggle-publish. The / route retrieves posts based on user roles; editors can see both published and unpublished posts, while viewers can access only published ones. The /toggle-publish route ensures that only editors can toggle the published status of a post.

  • utils/extractAndValidateToken.js: The code in this file extracts the session token from the request headers and uses the Descope client instance to validate the token.

  • utils/generatePosts.js: This file utilizes the Prisma ORM to interact with the SQLite database and inserts four posts into the database. It defines an array of post objects containing titles, bodies, and published statuses. The code then iterates through each post and inserts it into the database.

  • prisma/schema.prisma: This defines the data model for your application.

Now that you know what the code does, you can continue with the backend setup. Execute these commands to navigate into the project’s server folder and install all the dependencies:

cd descope-react-native-auth/server

yarn

Create a .env file in the server folder and add the following code:

DATABASE_URL="file:./dev.db"
DESCOPE_PROJECT_ID=<YOUR-DESCOPE-PROJECT-ID>

Note: Make sure you replace <YOUR-DESCOPE-PROJECT-ID> with the project ID that you obtained in the previous section.

To map the data model defined in the prisma/schema.prisma file to the SQLite database, execute this command:

npx prisma migrate dev --name init

Execute the following command to populate the database with some data:

node utils/generatePosts.js

The backend is now fully set up. You can run the development server using this command:

yarn dev

Modifying auth flows

Return to your Descope console, select Flows from the sidebar, and click the sign-up-or-in flow to open the flow details page:

Fig: Opening the sign-up-or-in Flow

Currently, the flow uses SMS OTP as the fallback MFA. You need to modify it to use email OTP as the fallback MFA.

Begin by double-clicking the Welcome Screen to open it in design mode:

Fig: Updating the Welcome Screen

Replace the phone number input with an email input and click Done in the top-right corner:

Fig: Adding an email input

You now need to update the flow’s OTP verification method from SMS to email. To do so, delete the Sign Up or In / OTP / SMS action, the Verify OTP screen, and the Verify Code / OTP / SMS action, as highlighted here:

Fig: Deleting the SMS OTP screen and actions

Add the email OTP verification method by clicking the + icons and selecting Action:

Fig: Adding an action

From the Add Action modal, select Sign Up or In / OTP / Email and add the action where you deleted the SMS OTP one. Make sure to connect to the other screens, conditions, and actions as necessary.

The updated part of your flow should now be connected to the Welcome Screen and Is new user? condition:

Fig: Updated SMS to email OTP verification

You now need to update the Is phone verified? condition as you are now using email verification. Open it in design mode and use Is email verified? as the Step name. Also, change the If and Else If condition names to Email verified and Email not verified, respectively. Lastly, change the key for the If condition to user.verifiedEmail and click Done:

Fig: Updated condition details

Finally, update the highlighted section of the flow to use email OTP like you did in the previous step. The only difference is to make sure you choose the Update User / OTP / Email from the Add Action modal:

Fig: Part of the flow to update

The updated part of the flow should now use an email OTP to verify the user’s email address:

Fig: Updated flow

Here is a preview of the complete flow:

Fig: Final updated flow

Save the flow and proceed to the next section.

Setting up roles

You can now continue to set up your application’s two roles, editors and viewers, in the Descope console. From the console sidebar, select Authorization and click the + Role button:

Fig: Creating a new role

In the Add Role modal, enter editor as the Name and Can access all posts and toggle their published status as the Description:

Fig: Adding role details

Repeat the same process and create a role for the viewer. Enter viewer as the Name and Can only access published posts as the Description.

The Authorization page should now display the two roles you just added:

Fig: Authorization page with the two roles added

You’ll assign these roles to the users later on.

Integrating React Native with Descope

With the server, flows, and roles set up, you can now create the React Native application and integrate Descope.

Implementing authentication with Descope

Here, you will use Descope to handle your app’s authentication and conditionally render in-app data based on the role of the currently logged-in user.

Start by executing the following command in the project root folder to initialize a React Native application:

npx react-native@latest init DescopeReactNativeAuth

cd into the React Native application folder and install the Descope React Native SDK using the following commands:

cd DescopeReactNativeAuth

yarn add @descope/react-native-sdk

Rename the App.tsx file to App.js. You will use JavaScript in this guide. Then open the file and replace the existing code with the following:

import React from "react";
import { SafeAreaView, StatusBar } from "react-native";
import { AuthProvider } from "@descope/react-native-sdk";
import Flow from "./components/Flow";

function App() {
    return (
     <AuthProvider projectId="<YOUR-DESCOPE-PROJECT-ID>">
         <SafeAreaView style={{ flex: 1, padding: 10 }}>
                <StatusBar barStyle="light-content" />
                <Flow />
         </SafeAreaView>
     </AuthProvider>
    );
}

export default App;

This code imports the Descope React Native SDK and wraps the app with the authentication provider. This activates the SDK.

Create a new file named components/Flow.js and add the following code to handle the authentication and the display of posts:

import { useFlow, useDescope, useSession } from "@descope/react-native-sdk";
import React, { useCallback, useEffect, useState } from "react";
import {
    Alert,
    Button,
    Linking,
    SafeAreaView,
    ScrollView,
    Text,
    TouchableOpacity,
    View,
} from "react-native";
import DetailsModal from "./DetailsModal";
import { styles } from "../styles";

export default function Flow() {
    const [posts, setPosts] = useState(null);
    const [selectedPost, setSelectedPost] = useState(null);

    const flow = useFlow();
    const { session, clearSession, manageSession } = useSession();
    const { logout } = useDescope();

    return (
     <SafeAreaView style={styles.container}>
         {session ? (
             <View style={styles.loggedInContainer}>
                 <DetailsModal
                     selectedPost={selectedPost}
                     setSelectedPost={setSelectedPost}
                     session={session}
                     togglePostPublish={togglePostPublish}
                 />

                 <View style={styles.header}>
                     <Text style={styles.headerText}>
                         Hello, {session.user.name.split(" ")[0]}
                     </Text>
                     <TouchableOpacity
                         style={styles.logoutButton}
                         onPress={handleLogout}
                     >
                         <Text style={styles.logoutText}>Log out</Text>
                     </TouchableOpacity>
                 </View>

                 <Text style={styles.sectionTitle}>Available Posts</Text>

                 <ScrollView style={styles.scrollView}>
                     {posts &&
                         posts.map((post) => (
                             <TouchableOpacity
                                 key={post.id}
                                 onPress={() => setSelectedPost(post)}
                                 style={styles.postContainer}
                             >
                                 <Text style={styles.postTitle}>
                                     {post.title}
                                 </Text>
                                 {session.user.roleNames.includes(
                                     "editor"
                                 ) && (
                                     <Text style={styles.editorPostInfo}>
                                         {post.published
                                             ? "Published"
                                             : "Not published"}
                                     </Text>
                                 )}
                             </TouchableOpacity>
                         ))}
                 </ScrollView>
             </View>
         ) : (
             <View style={styles.loggedOutContainer}>
                 <Text style={styles.welcomeText}>Welcome!</Text>
                 <Button onPress={startFlow} title="LOG IN" />
             </View>
         )}
     </SafeAreaView>
    );
}

This component initializes the state variables using useState to manage posts and selected posts. It also utilizes the Descope React Native SDK hooks (useFlow, useSession, and useDescope) to handle authentication-related functionality. Within the render method, it conditionally renders data based on whether there is an active session.

If there is an active session, the component displays a header with the user’s name, a logout button, and a list of the posts fetched from the backend based on the user’s role. If the user has the editor role, it also displays a badge that shows the post’s published status. It also renders a modal that displays a post’s details when a user taps on a certain post. If there is no active session, the component displays a welcome message and a login button.

Let’s add the functions that are called in the render method. Begin by adding the startFlow function that allows the user to sign up or log in:

const startFlow = async () => {
    try {
     const resp = await flow.start(
         "https://auth.descope.io/<YOUR-DESCOPE-PROJECT-ID>?flow=sign-up-or-in",
         "<YOUR-APP-LINKING-URL>"
     );
     await manageSession(resp.data);
    } catch (error) {
     console.log("Start flow error:", error);
    }
};

This function initiates the authentication process with the provided flow URL. The flow.start() method presents the flow screens that you defined earlier in a sandboxed browser view. Once the user completes the authentication process, it returns a JSON Web Token (JWT) response. This method takes in the flow URL (where the flow is hosted) and an optional app link URL if you are targeting the Android OS. Once the flow.start() function returns the JWT, you call the manageSession() function with the returned data to set an active user’s function.

Remember to replace <YOUR-DESCOPE-PROJECT-ID> with the Descope project ID that you obtained earlier. You will replace <YOUR-APP-LINKING-URL> with the appropriate value after you set up app linking.

Add the following useEffect hook that handles the app links:

useEffect(() => {
    Linking.addEventListener("url", async (event) => {
     if (event.url.includes("<YOUR-APP-LINKING-URL>")) {
         try {
             await flow.exchange(event.url);
         } catch (error) {
             console.log(error);
         }
     }
    });

    return () => {
     Linking.removeAllListeners("url");
    };
}, [flow]);

Inside the hook, you set up an event listener for incoming app-linking events. When an app link with the type url occurs, the callback function checks if the URL contains your app linking URL. If it does, you invoke the flow.exchange() method with the event.url as the argument. This function exchanges the URL for a JWT response.

Note: You will replace <YOUR-APP-LINKING-URL> later.

Next, add the handleLogout() function that allows the user to log out of your application:

const handleLogout = useCallback(() => {
    logout();
    clearSession();
}, [logout]);

This code invokes two functions: logout() to invalidate the current JWTs and clearSession() to set the session variable to undefined.

Fetching posts from the server

With all the authentication-related functionality set up, you can now add the function responsible for fetching the posts from the server:

const fetchPosts = async () => {
    try {
     const res = await fetch("http://<YOUR-IP-ADDRESS>:3000", {
         headers: {
             Authorization: `Bearer ${session.sessionJwt}`,
         },
     });

     const data = await res.json();

     if (!res.ok) {
         // If the response status is not in the range 200-299, throw an error
         throw new Error(data.message);
     } else {
         setPosts(data.posts);
     }
    } catch (error) {
     console.log("Error", error.message);

     if (error.message === "Session expired") {
         Alert.alert("Session expired", "Please log in", [
             // Redirect to the unauthenticated user's screen
             {
                 text: "Cancel",
                 onPress: () => {
                     logout();
                     clearSession();
                 },
                 style: "cancel",
             },
             // Start the login flow
             {
                 text: "LOG IN",
                 onPress: () => {
                     logout();
                     clearSession();
                     startFlow();
                 },
                 style: "default",
             },
         ]);
     }
    }
};

This function initiates a fetch request to the provided server endpoint. The request includes an Authorization header that contains a session JWT token for authentication. Upon receiving the response, it parses the JSON data. If the response is successful (res.ok), it updates the component’s state with the fetched posts using the setPosts function. However, if an error occurs during the fetch operation, the catch block is triggered, and it checks if the error indicates a session expiration.

If the session has expired, it displays an alert prompting the user to log in again. The alert provides options to either cancel the action, which logs out the user and clears the session, or start the login flow again.

Note: Make sure you replace <YOUR-IP-ADDRESS> with your computer’s IP address.

Add this useEffect to make sure that the posts are fetched only when there is an active session:

useEffect(() => {
    if (session) {
     fetchPosts();
    }
}, [session]);

Adding post details screen and editor actions

As the introduction says, the editors need to be able to toggle a post’s published status. To support this functionality, add the following function to the same file:

const togglePostPublish = async () => {
    try {
     const res = await fetch(
         "http://<YOUR-IP-ADDRESS>:3000/toggle-publish",
         {
             headers: {
                 Authorization: `Bearer ${session.sessionJwt}`,
                 "Content-Type": "application/json",
             },
             method: "POST",
             body: JSON.stringify({ postId: selectedPost.id }),
         }
     );

     const data = await res.json();

     if (!res.ok) {
         throw new Error(data.message);
     }

     // Update selected post with the new data. This will help to update the editor actions buttons.
     setSelectedPost(data.updatedPost);

     // Fetch updated posts
     fetchPosts();
    } catch (error) {
     console.log(error);
    }
};

Note: Make sure you replace <YOUR-IP-ADDRESS> with your computer’s IP address.

Next, create a new file named DetailsModal.js in the components folder and add the following code that displays the post details as well as buttons to manage posts for the editors:

import {
    Alert,
    Modal,
    ScrollView,
    Text,
    TouchableOpacity,
    View,
} from "react-native";
import { styles } from "../styles";

export default function DetailsModal({
    selectedPost,
    setSelectedPost,
    session,
    togglePostPublish,
}) {
    return (
     <Modal
         animationType="fade"
         visible={selectedPost !== null}
         onRequestClose={() => setSelectedPost(null)}
     >
         {selectedPost && (
             <View style={styles.modalContainer}>
                 {session.user.roleNames.includes("editor") && (
                     <View style={styles.editorActionsContainer}>
                         <Text style={styles.sectionTitle}>
                             Editor Actions
                         </Text>
                         <View style={styles.actionButtonsContainer}>
                             <TouchableOpacity
                                 onPress={() => togglePostPublish()}
                                 style={styles.actionButton}
                             >
                                 <Text style={styles.actionButtonText}>
                                     {selectedPost.published
                                         ? "Unpublish"
                                         : "Publish"}
                                 </Text>
                             </TouchableOpacity>

                             <TouchableOpacity
                                 style={styles.actionButton}
                                 onPress={() =>
                                     Alert.alert(
                                         "Editor alert",
                                         "You are an editor. You can edit this post"
                                     )
                                 }
                             >
                                 <Text style={styles.actionButtonText}>
                                     Edit
                                 </Text>
                             </TouchableOpacity>
                         </View>
                     </View>
                 )}

                 <View style={styles.modalHeader}>
                     <Text style={styles.modalTitle}>
                         {selectedPost.title}
                     </Text>
                     <TouchableOpacity
                         style={styles.closeButton}
                         onPress={() => setSelectedPost(null)}
                     >
                         <Text style={styles.closeButtonText}>Close</Text>
                     </TouchableOpacity>
                 </View>

                 <ScrollView contentContainerStyle={styles.scrollContainer}>
                     <Text style={styles.modalBody}>
                         {selectedPost.body}
                     </Text>
                 </ScrollView>
             </View>
         )}
     </Modal>
    );
}

This component is responsible for displaying the details of the selected post. It receives props such as selectedPost, setSelectedPost, session, and togglePostPublish. The modal is conditionally rendered based on whether a post is selected (selectedPost !== null). If the user has an editor role (session.user.roleNames.includes('editor')), additional editor actions, such as publishing, unpublishing, and editing the post, are displayed.

Styling the application

Lastly, create a styles.js file in the React Native application root folder and add the following code to style the application:

import { StyleSheet } from "react-native";

const styles = StyleSheet.create({
    container: {
     flex: 1,
    },
    loggedInContainer: {
     flex: 1,
     backgroundColor: "#fff",
    },
    loggedOutContainer: {
     flex: 1,
     justifyContent: "center",
     alignItems: "center",
     backgroundColor: "#fff",
    },
    header: {
     flexDirection: "row",
     justifyContent: "space-between",
     alignItems: "center",
     backgroundColor: "#fff",
     paddingHorizontal: 16,
     paddingVertical: 12,
     borderBottomWidth: 1,
     borderBottomColor: "#ccc",
    },
    headerText: {
     fontSize: 18,
     fontWeight: "bold",
    },
    logoutButton: {
     paddingHorizontal: 12,
     paddingVertical: 8,
     borderRadius: 6,
     backgroundColor: "#f00",
    },
    logoutText: {
     color: "#fff",
     fontSize: 16,
     fontWeight: "bold",
    },
    sectionTitle: {
     fontSize: 18,
     fontWeight: "bold",
     textAlign: "center",
     marginTop: 8,
    },
    scrollView: {
     flex: 1,
     paddingHorizontal: 16,
     paddingTop: 12,
     backgroundColor: "white",
    },
    postContainer: {
     marginBottom: 8,
     padding: 12,
     backgroundColor: "#f9f9f9",
     borderRadius: 8,
     shadowColor: "#000",
     shadowOffset: {
         width: 0,
         height: 2,
     },
     shadowOpacity: 0.25,
     shadowRadius: 4,
     elevation: 2,
    },
    postTitle: {
     fontSize: 16,
     fontWeight: "bold",
    },
    editorPostInfo: {
     fontSize: 12,
     marginTop: 2,
     backgroundColor: "green",
     alignSelf: "flex-start",
     paddingHorizontal: 3,
     paddingVertical: 1,
     borderRadius: 5,
     color: "white",
    },
    welcomeText: {
     fontSize: 20,
     marginBottom: 20,
    },
    modalContainer: {
     flex: 1,
     backgroundColor: "#fff",
    },
    modalHeader: {
     width: "100%",
     flexDirection: "row",
     justifyContent: "space-between",
     gap: 10,
     alignItems: "center",
     backgroundColor: "#f0f0f0",
     paddingVertical: 10,
     paddingHorizontal: 20,
    },
    modalTitle: {
     fontSize: 20,
     fontWeight: "bold",
     flex: 1,
    },
    closeButton: {
     backgroundColor: "#f00",
     paddingVertical: 8,
     paddingHorizontal: 16,
     borderRadius: 5,
    },
    closeButtonText: {
     color: "#fff",
     fontSize: 16,
     fontWeight: "bold",
    },
    editorActionsContainer: {
     marginTop: 20,
     paddingHorizontal: 20,
    },
    sectionTitle: {
     fontSize: 18,
     fontWeight: "bold",
     marginBottom: 10,
     textAlign: "center",
    },
    actionButtonsContainer: {
     flexDirection: "row",
     justifyContent: "center",
     gap: 30,
    },
    actionButton: {
     backgroundColor: "#007bff",
     borderRadius: 5,
     paddingVertical: 10,
     paddingHorizontal: 20,
     marginBottom: 10,
    },
    actionButtonText: {
     color: "#fff",
     fontSize: 16,
     fontWeight: "bold",
    },
    scrollContainer: {
     flexGrow: 1,
     paddingHorizontal: 20,
     paddingTop: 10,
    },
    modalBody: {
     fontSize: 16,
     lineHeight: 24,
    },
});

export { styles };

Setting up app linking for Android OS

In the previous section, you left a few placeholders for <YOUR-APP-LINKING-URL>. In this section, you’ll create the app-linking URL and replace the placeholders with the actual values.

An Android app link is a special type of deep link that allows website URLs to automatically launch an activity in your application without requiring further input from the user. Android app links leverage the Digital Asset Links API to verify that your application has permission from the website to automatically open links for that specific domain.

You initially need to declare an association between your website and the Android application’s intent filters by hosting a Digital Asset Links JSON file at https://<YOUR-DOMAIN>/.well-known/assetlinks.json. The assetlinks.json file should take the following format:

[
    {
     "relation": ["delegate_permission/common.handle_all_urls"],
     "target": {
         "namespace": "android_app",
         "package_name": "<YOUR-APPLICATION-ID>",
         "sha256_cert_fingerprints": ["<YOUR-APP-SHA256-FINGERPRINT>"]
     }
    }
]

You can use the Google Statement List Tester and Generator to generate and test your Digital Asset Links JSON file.

Make sure you replace <YOUR-APPLICATION-ID> with your app’s application ID, which you can find in the android/app/build.gradle file. Next, execute the following command in the terminal to generate your app’s SHA256 fingerprint:

keytool -list -v -keystore android/app/debug.keystore

In the terminal prompt, enter android as the keystore password. Once the certificate fingerprints are generated, copy the value of the SHA256 fingerprint and replace the <YOUR-APP-SHA256-FINGERPRINT> placeholder with it.

You can host the assetlinks.json file on any site as long as it’s publicly available at https://<YOUR-DOMAIN>/.well-known/assetlinks.json. This guide uses GitHub Pages.

Navigate to the Create a new repository page on GitHub and create a public repository with the name <YOUR-GITHUB-USERNAME>.github.io. Make sure you replace <YOUR-GITHUB-USERNAME> with your GitHub username.

Once the repo is created, select creating a new file:

Fig: Creating a new file

Provide the file name as index.html, add the following code, which adds some content for the site, and commit changes:

<!DOCTYPE html>
<html lang="en">
    <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Document</title>
    </head>
    <body>
     <h1>Descope React Native Auth</h1>
    </body>
</html>

Next, add a new file named .well-known/assetlinks.json to the same repo and add the contents of the assetlinks.json file that you created earlier:

[
    {
     "relation": ["delegate_permission/common.handle_all_urls"],
     "target": {
         "namespace": "android_app",
         "package_name": "com.descopereactnativeauth",
         "sha256_cert_fingerprints": [
             "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
         ]
     }
    }
]

Note: Your SHA256 fingerprint might be different.

In the same repo, create a new file named _config.yml and add the following content to include the .well-known directory in the Jekyll build process:

include: [".well-known"]

With all the files created, GitHub automatically deploys the website. You can confirm this by navigating to https://<YOUR-GITHUB-USERNAME>.github.io. You can also confirm that the assetlinks.json file is publicly accessible by navigating to https://<YOUR-GITHUB-USERNAME>.github.io/.well-known/assetlinks.json.

You’ll use https://<YOUR-GITHUB-USERNAME>.github.io as the app-linking URL. At this point, you can go back to the components/Flow.js file and replace all the app link URL placeholders with this value.

The next step is to add intent filters for the app link verification. Open the android/app/src/main/AndroidManifest.xml file and the following intent filter just before the closing </activity> tag:

<intent-filter android:autoVerify="true"> <!-- autoVerify required for app links -->
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="<YOUR-GITHUB-USERNAME>.github.io" />
</intent-filter>

Note: Make sure you replace <YOUR-GITHUB-USERNAME> with your GitHub username.

App linking is now fully set up in your application.

Testing the application

Before you can test the application, open the android/build.gradle file and change the minSdkVersion from 21 to 23:

minSdkVersion = 23

Next, make sure the Express server is running by executing the following commands in the terminal in the project root folder:

cd server

yarn dev

In another terminal, execute the following commands to start the React Native Metro server:

cd DescopeReactNativeAuth

yarn start

Next, make sure your emulator is running and execute the following command in a separate terminal to start the Android app on the emulator:

cd DescopeReactNativeAuth

yarn android

Demoing the app

Once the app starts, you should see the Welcome Screen:

Fig: Welcome screen

Click the LOG IN button to sign up / log in to the app. On the authentication page, input your email address and click Sign In With Passkeys:

Fig: Authentication page

Choose a device for your passkey and provide the passkey:

Fig: Providing the passkey

Descope sends an OTP to the provided email address, and then you are prompted to provide the OTP:

Fig: OTP verification

Since this is your first time signing up, you are asked to provide your full name:

Fig: Providing your full name

After providing your full name, you are navigated back to the app:

Fig: Navigated back to the app

You can now log out of the app and navigate to the Descope console to assign a role to the user. On the Descope console, select Users from the sidebar and edit the details of the user you just registered:

Fig: Editing the user details

On the user details modal, select + Add Tenant / Role, add the Editor role to the user, and click Save:

Fig: Adding the editor role

Log in to the app again. You will now be able to see all four posts as well as their published status:

Fig: Logged in as editor

You can select a published post to see the post details, as well as the buttons to publish/unpublish and edit a post. Select the Unpublish button to unpublish the post:

Fig: Unpublishing a post

Now, log out of the application, go to the Descope console, and change the user’s role to viewer. Then log back into the application. Now, you can access only a single post:

Fig: Logged in as viewer

This confirms that the RBAC implementation is working as expected.

You can access the full code for the project on GitHub.

Conclusion

In this guide, you learned about the complexities associated with traditional authentication methods in React Native applications, such as the need to keep updating your codebase to accommodate the changing authentication needs, understanding security intricacies, and diverting focus from building the app’s core functionality.

You overcame these challenges by integrating Descope into your React Native application to simplify the process for customizing your app’s authentication flows and setting up RBAC.

Descope is a leading customer authentication and identity management platform that allows you to use no-code workflows to design your application’s user authentication process. With Descope, you can easily add passwordless authentication, MFA, fraud prevention, single sign-on (SSO), and much more to your application. Try Descope for your authentication needs today!