ForgeRock SDKs 3.3

Build a protected React Native mobile app: iOS

This is part one of a multi-part series covering the basics of developing a ForgeRock-protected, mobile app with React Native. This part focuses on developing the iOS bridge code along with a minimal React UI to authenticate a user.

First, what is a protected mobile app?

A protected app (client or server) is simply an app that uses some type of access artifact to verify a user’s identity (authentication) and permissions (authorization) prior to giving access to a resource. This "access artifact" can be a session cookie, an access token, or an assertion, and is shared between the entities of a system.

Additionally, a protected mobile app (client) is responsible for providing a user with methods for acquiring, using, and removing this access artifact upon request. The focus of this guide is implementing these capabilities using the ForgeRock iOS SDK within a React Native app.

What you will learn

"Bridge code" development is a concept common to mobile apps built using hybrid technologies. "Hybrid" is a term used when a portion of the mobile app is written in a language that is not native to the platform (Android and Java or iOS and Swift). React Native, a cross-platform, view-library from Facebook, requires this bridging code to provide the React (JavaScript) layer access to native (Swift in this case) APIs or dependencies.

We also touch on some of the concepts and patterns popularized by the React library. Since ForgeRock doesn’t (as of this writing) provide a React Native version of our SDK, we present this how-to as a guide to basic development of "bridge code" for connecting our iOS SDK to the React Native layer.

This guide covers how to implement the following application features using version 3 of the ForgeRock iOS and JavaScript SDKs:

  1. Authentication through a simple journey/tree.

  2. Requesting OAuth/OIDC tokens.

  3. Requesting user information.

  4. Logging a user out.

This is not a guide on how to build a React Native app

How to architect or construct React Native apps is outside the scope of this guide. It’s also worth noting that there are many React Native libraries and frameworks for building mobile applications. What is "best" is highly contextual to the product and user requirements for any given project.

To simply demonstrate our SDK integration, we use a few libraries to simplify the development of the application. We chose React Navigation as the navigation library, and NativeBase as the UI library, but neither is featured heavily in this part of the multi-part guide.

Use this guide

This is a "hands on" guide. We are providing the mobile app and resource server for you. You can find the repo on GitHub to follow along. All you’ll need is your own ForgeRock® Identity Cloud tenant or ForgeRock® Access Management (AM) service. If you don’t have access to either, and you are interested in the ForgeRock® Identity Platform, reach out to a representative today, and we’ll be happy to get you started.

Two ways of using this guide

  1. Follow along by building portions of the app yourself: continue by ensuring you can meet the requirements below

  2. If you’re just curious about the bridge code and React Native implementation details: skip to Implement the iOS bridge code for authentication.

Requirements

Knowledge requirements

  1. Xcode, the iOS Simulator and related tools.

  2. Swift and CocoaPods.

  3. JavaScript and the npm ecosystem of modules.

  4. The command line interface (such as Terminal, Shell or Bash).

  5. Core Git commands (such as clone, and checkout).

  6. Functional components: we use the functional-style, React components.

  7. Hooks: local state and side effects are managed with this concept.

  8. Context API: global state is managed with this concept.

Technical requirements

  • Admin access to an instance of ForgeRock Identity Cloud or Access Management (AM).

  • Xcode 12 or higher with the developer tools installed.

  • Node.js 14 or higher & npm 7 or higher (please check your version with node -v and npm -v).

  • A tool or service to generate a security certificate and key (self-signed is fine).

ForgeRock setup

Step 1. Create a simple login journey/tree

You need a simple username and password authentication journey/tree for this guide.

  1. Create a Page Node and connect it to the start of the journey.

  2. Add the Username Collector and Password Collector nodes within the Page Node.

  3. Add the Data Store Decision node and connect it to the Page Node.

  4. Connect the True outcome of the decision node to the Success node.

  5. Connect the False outcome of the decision node to the Failure node.

username password journey
Figure 1. Example journey from Identity Cloud

Step 2. Create an OAuth client

Within the ForgeRock server, create a public, "Native", OAuth client for the mobile client app with the following values:

  • Client name/ID: ReactNativeOAuthClient

  • Client type: Public

  • Secret: <leave empty>

  • Scopes: openid profile email

  • Grant types: Authorization Code Refresh Token

  • Implicit consent: enabled

  • Redirection URLs/Sign-in URLs: https://com.example.reactnative.todo/callback

  • Token authentication endpoint method: none

Each of the above is required, so double check you set each of the items correctly. If you don’t, you’ll likely get an "invalid OAuth client" error in Xcode logs.

oauth client configuration
Figure 2. Example OAuth client from Identity Cloud

Step 3. Create a test user

Create a test user (identity) in your ForgeRock server within the realm you will use.

Or, if you are using AM, click Identities in the left navigation. Use the following instructions to create a user:

  1. Click Add Identity to view the create user form.

  2. In a separate browser tab, visit this UUID v4 generator and copy the UUID.

  3. Switch back to AM and paste the UUID into the User ID input.

  4. Provide a password and email address.

You will use this UUID as the username for logging into the app.

Local project setup

Step 1. Clone the project

First, clone the React Native Sample project to your local computer, cd (change directory) into the project folder, and checkout the branch for this guide:

git clone https://github.com/ForgeRock/forgerock-react-native-sample.git
cd forgerock-react-native-sample
git checkout blog/build-protected-app/part-one/start

In addition, there’s a branch that represents the completion of this guide. If you get stuck, you can visit blog/build-protected-app/part-one/complete branch in GitHub and use it as a reference.

The branch used in this guide is an overly-simplified version of the sample app found in the main Git branch within the reactnative-todo directory. This "full version" is a to-do application that communicates with a protected REST API.
react native todos screen
Figure 3. The to-do sample app

Step 2. Install the project dependencies

This React Native app requires two types of dependencies:

  1. JavaScript and its Node Package Modules

  2. Swift with CocoaPods.

First, let’s install the JavaScript dependencies. Within the project directory: reactnative-todo/ (file and directory references are from this location), use the following command:

npm install

Once the above command finishes, cd into the ios directory and install the needed CocoaPods. When done, you can return to the project directory.

cd ios
pod install
cd ..

Step 3. Create the .env.js file

Using the ForgeRock server settings from above, create the .env.js file from the .env.js.template file within the project. Add your relevant values to configure all the important server settings to your project. Not all variables will need values at this time.

A hypothetical example (required variables commented):

/**
 * Avoid trailing slashes in the URL string values below
 */
const AM_URL = 'https://auth.forgerock.com/am'; // Required; enter _your_ AM URL
const API_PORT = 8080; // Required; default port is 8080
const API_BASE_URL = 'http://localhost'; // Required; default domain is http://localhost
const DEBUGGER_OFF = true;
const REALM_PATH = 'alpha'; // Required
const REST_OAUTH_CLIENT = 'RestOAuthClient';
const REST_OAUTH_SECRET = '0Rtg8s3s23w4e35L7zHr0dfkZZ';

Descriptions of relevant values:

  • AM_URL: The URL that references AM itself (for Identity Cloud, the URL is likely https://<tenant-name>.forgeblocks.com/am).

  • API_PORT and API_BASE_URL just need to be "truthy" (not 0 or empty string) right now to avoid errors and will be used in a future part of this series.

  • DEBUGGER_OFF: When true, this disables the debugger statements in the JavaScript layer. These debugger statements are for learning the integration points at runtime in your browser. When the browser’s developer tools are open, the app pauses at each integration point. Code comments above each integration point explain its use.

  • REALM_PATH: The realm of your ForgeRock server (likely root, alpha or bravo).

  • REST_OAUTH_CLIENT and REST_OAUTH_SECRET: These will be used in a future part of this series, so any string value will do.

Build and run the project

Now that everything is set up, build and run the to-do app project.

  1. Open your Finder application and find the following file ios/reactnativetodo.xcworkspace.

  2. Double click this file to open and load the project within Xcode.

  3. Once Xcode is ready, select iPhone 11 or higher as the target for the device simulator on which to run the app.

  4. Now, click the build/play button to build and run this application in the target simulator.

With everything up and running, you will need to rebuild the project with Xcode when you modify the bridge code (Swift files). But, when modifying the React Native code, it will use "hot module reloading" to automatically reflect the changes in the app without having to manually rebuild the project.

If the app doesn’t build

  1. Make sure libFRAuth.a is added to your Target’s Frameworks, Libraries, and Embedded Content under the General tab.

  2. Make sure the Metro server is running; npx react-native start if you want to run it manually.

  3. Bridge code has been altered, so be aware of API name changes.

  4. If you get the error, [!] CocoaPods could not find compatible versions for pod "FRAuth", run pod repo update then pod install.

Xcode, iOS Simulator and Safari dev tools

We recommend the use of iPhone 11 or higher as the target for the iOS Simulator. When you first run the build command in Xcode (clicking the "play" button), it takes a while for the app to build, the OS to load, and app to launch within the Simulator. Once the app is launched, rebuilding it is much faster if the changes are not automatically "hot reloaded" when made in the React layer.

react native home screen
Figure 4. To-do app home screen
Only the home screen will render successfully at this moment. If you click on the Sign In button, it won’t be fully functional. This is intended as you will develop this functionality throughout this tutorial.

Once the app is built and running, you will have access to all the logs within Xcode’s output console. Both the native and JavaScript logs display here. Because of this, there’s quite a lot of output, so you may want to use it only when the Safari console does not provide enough information for debugging purposes.

react native xcode login response
Figure 5. Xcode log output

For additional tooling, click "Device" within the top menu, and then select "Shake". This triggers the React Native dev tools, allowing you to reload the app, inspect the UI, as well as other actions.

Due to a particular confusing bug in React Native, we do not recommend using the Chrome debugger, but recommend using Safari for debugging. To use Safari for debugging the React code, follow the instructions found in the React Native docs.
react native safari devtools
Figure 6. Safari dev tools

Tips if the home screen doesn’t render

  1. Restart the app (in Xcode) and Metro (in terminal).

  2. Didn’t work? Using Xcode, clean the build folder and rebuild/rerun the app.

  3. If that doesn’t work, remove the following from the reactnative-todo directory: node_modules, package-lock.json, ios/.Pods, ios/Podfile.lock, and then reinstall dependencies with npm i and within the ios/ directory pod install.

  4. If you’re still having issues, within the simulator, click the Home button and long press the React Todo application to .remove it. Then, restart from the project Xcode.

  5. You can also use Device > Erase All Content and Settings if the problem persists.

Implement the iOS bridge code for authentication

We first need to review the files that allow for the "bridging" between our React Native project and our native SDKs. Within Xcode, navigate to the ios/reactnativetodo directory, and you will see a few important files:

  • reactnativetodo-Bridging-Header.h: Header file that exposes the React Native bridging module and the FRAuth module into the Swift context.

  • FRAuthSampleBridge.m: The module file that defines the exported interfaces of our bridging code.

  • FRAuthSampleBridge.swift: The main Swift bridging code that provides the callable methods for the React Native layer.

  • FRAuthSampleStructs.swift: Provides the structs for the Swift bridging code.

  • FRAuthSampleHelpers.swift: Provides the extensions to often used objects within the bridge code.

  • FRAuthConfig: A .plist file that configures the iOS SDK to the appropriate ForgeRock server.

We provide the header file as is. The file’s creation, naming and use requires very specific conventions that are outside the scope of this tutorial. You will not need to modify it.

The remainder of the files within the workspace are automatically generated when you create a React Native project with the CLI command, so you can ignore them.

Step 2. Configure your .plist file

Within Xcode’s directory/file list section (aka Project Navigator), complete the following:

  1. Find FRAuthConfig.plist file within the ios/reactnativetodos directory.

  2. Add the name of your ForgeRock cookie.

  3. Add the OAuth client you created from above.

  4. Add your ForgeRock URLs.

  5. Add the login tree you created above.

A hypothetical example (your values may vary):

  <dict>
    <key>forgerock_cookie_name</key>
-   <string></string>
+   <string>e1babb394ea5130</string>
    <key>forgerock_enable_cookie</key>
    <true/>
    <key>forgerock_oauth_client_id</key>
    <string>ReactNativeOAuthClient</string>
    <key>forgerock_oauth_redirect_uri</key>
    <string>https://com.example.reactnative.todo/callback</string>
    <key>forgerock_oauth_scope</key>
    <string>openid profile email</string>
    <key>forgerock_oauth_url</key>
-   <string></string>
+   <string>https://auth.forgerock.com/am</string>
    <key>forgerock_oauth_threshold</key>
    <string>60</string>
    <key>forgerock_url</key>
-   <string></string>
+   <string>https://auth.forgerock.com/am</string>
    <key>forgerock_realm</key>
-   <string></string>
+   <string>alpha</string>
    <key>forgerock_timeout</key>
    <string>60</string>
    <key>forgerock_keychain_access_group</key>
    <string>org.reactjs.native.example.reactnativetodo</string>
    <key>forgerock_auth_service_name</key>
-   <string></string>
+   <string>UsernamePassword</string>
    <key>forgerock_registration_service_name</key>
-   <string></string>
+  <string>Registration</string>
</dict>

Descriptions of relevant values:

  • forgerock_cookie_name: If you have Identity Cloud, you can find this random string value under the Tenant Settings found in the top-right dropdown in the admin UI. If you have your own installation of AM, this is often iPlanetDirectoryPro.

  • forgerock_url & forgerock_oauth_url: The URL of AM within your ForgeRock server installation.

  • forgerock_realm: The realm of your ForgeRock server (likely root, alpha or beta).

  • forgerock_auth_service_name: This is the journey/tree that you use for login.

  • forgerock_registration_service_name: This is the journey/tree that you use for registration, but it will not be used until a future part of this tutorial series.

Step 3. Write the start() method

Staying within the reactnativetodo directory, find the FRAuthSampleBridge file and open it. We have some of the files already stubbed out and the dependencies are already installed. All you need to do is write the functionality.

For the SDK to initialize with the FRAuth.plist configuration from Step 2, write the start() function as follows:

  import Foundation
  import FRAuth
  import FRCore
  import UIKit

  @objc(FRAuthSampleBridge)
  public class FRAuthSampleBridge: NSObject {
    var currentNode: Node?

    @objc static func requiresMainQueueSetup() -> Bool {
      return false
    }

+   @objc func start(
+     _ resolve: @escaping RCTPromiseResolveBlock,
+     rejecter reject: @escaping RCTPromiseRejectBlock) {

+     /**
+      * Set log level to all
+      */
+     FRLog.setLogLevel([.all])
+
+     do {
+       try FRAuth.start()
+       let initMessage = "SDK is initialized"
+       FRLog.i(initMessage)
+       resolve(initMessage)
+     } catch {
+       FRLog.e(error.localizedDescription)
+       reject("Error", "SDK Failed to initialize", error)
+     }
+   }

    /**
     * Method for calling the `getUserInfo` to retrieve the user information from
     * the OIDC endpoint
     */
    @objc func getUserInfo(
      _ resolve: @escaping RCTPromiseResolveBlock,
      rejecter reject: @escaping RCTPromiseRejectBlock) {

@@ collapsed @@

The start() function above calls the iOS SDK’s start() method on the FRAuth class. There’s a bit more that may be required within this function for a production app. We’ll get more into this in a separate part of this series, but for now, let’s keep this simple.

Step 4. Write the login() method

Once the start() method is called and it has initialized, the SDK is now ready to handle user requests. Let’s start with login().

Just underneath the start() method we wrote above, add the login() method.

@@ collapsed @@

  @objc func start(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock) {

    /**
     * Set log level according to  all
     */
    FRLog.setLogLevel([.all])

    do {
      try FRAuth.start()
      let initMessage = "SDK is initialized"
      FRLog.i(initMessage)
      resolve(initMessage)
    } catch {
      FRLog.e(error.localizedDescription)
      reject("Error", "SDK Failed to initialize", error)
    }
  }

+ @objc func login(
+   _ resolve: @escaping RCTPromiseResolveBlock,
+   rejecter reject: @escaping RCTPromiseRejectBlock) {
+
+   FRUser.login { (user, node, error) in
+     self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
+   }
+ }

@@ collapsed @@

This login() function initializes the journey/tree specified for authentication. You call this method without arguments as it does not login the user. This initial call to the ForgeRock server will return the first set of callbacks that represents the first node in your journeyt/tree to collect user data.

Also, notice that we have a special "handler" function within the callback of FRUser.login(). This handleNode() method serializes the node object that the iOS SDK returns in a JSON string. Data passed between the "native" layer and the React layer is limited to strings. This method can be written in many ways and should be written in whatever way is best for your application. However, a unique use of the ForgeRock JavaScript SDK to convert this basic JSON of data into a decorated object for better ergonomics is used in this tutorial.

Step 5. Write the next() method

To finalize the functionality needed to complete user authentication, we need a way to iteratively call next until the tree completes successfully or fails. To do this, continue in the bridge file, and add a private method called handleNode().

First, we will write the decoding of the JSON string and prepare the node for submission.

@@ collapsed @@

  @objc func login(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock) {

    FRUser.login { (user, node, error) in
      self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
    }
  }

+ @objc func next(
+   _ response: String,
+   resolve: @escaping RCTPromiseResolveBlock,
+   rejecter reject: @escaping RCTPromiseRejectBlock) {
+
+   let decoder = JSONDecoder()
+   let jsonData = Data(response.utf8)
+   if let node = self.currentNode {
+     var responseObject: Response?
+     do {
+       responseObject = try decoder.decode(Response.self, from: jsonData)
+     } catch  {
+       FRLog.e(String(describing: error))
+       reject("Error", "UnknownError", error)
+     }
+
+     let callbacksArray = responseObject!.callbacks ?? []
+
+     for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
+       if let thisCallback = nodeCallback as? SingleValueCallback {
+         for (innerIndex, rawCallback) in callbacksArray.enumerated() {
+           if let inputsArray = rawCallback.input, outerIndex == innerIndex,
+             let value = inputsArray.first?.value {
+
+             thisCallback.setValue(value.value as! String)
+           }
+         }
+       }
+     }
+   } else {
+     reject("Error", "UnknownError", nil)
+   }
+ }

@@ collapsed @@

Now that you’ve prepared the data for submission, introduce the node.next call from the iOS SDK. Then, handle the subsequent node returned from the next call, or process the success or failure representing the completion of the journey/tree.

@@ collapsed @@

      for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
        if let thisCallback = nodeCallback as? SingleValueCallback {
          for (innerIndex, rawCallback) in callbacksArray.enumerated() {
            if let inputsArray = rawCallback.input, outerIndex == innerIndex,
              let value = inputsArray.first?.value {

              thisCallback.setValue(value)
            }
          }
        }
      }

+     node.next(completion: { (user: FRUser?, node, error) in
+       if let node = node {
+         self.handleNode(user, node, error, resolve: resolve, rejecter: reject)
+       } else {
+         if let error = error {
+           reject("Error", "LoginFailure", error)
+           return
+         }
+
+         let encoder = JSONEncoder()
+         encoder.outputFormatting = .prettyPrinted
+         if let user = user,
+           let token = user.token,
+           let data = try? encoder.encode(token),
+           let accessInfo = String(data: data, encoding: .utf8) {
+
+           resolve(["type": "LoginSuccess", "accessInfo": accessInfo])
+         } else {
+           resolve(["type": "LoginSuccess", "accessInfo": ""])
+         }
+       }
+     })
    } else {
      reject("Error", "UnknownError", nil)
    }
  }

@@ collapsed @@

The above code handles a limited number of callback types. Handling full authentication and registration journeys/trees requires additional callback handling. To keep this tutorial simple, we’ll focus just on SingleValueCallback type.

Step 6. Write the logout() bridge method

Finally, add the following lines of code to enable logout for the user:

@@ collapsed @@

    } else {
      reject("Error", "UnknownError", nil)
    }

+   @objc func logout() {
+     FRUser.currentUser?.logout()
+   }
  }

@@ collapsed @@

Build a React Native form for simple login

Step 1. Initialize the SDK

First, let’s review how the application renders the home view:

index.js > src/index.js > src/router.js > src/screens/home.js

Open up the second file in the above sequence, the src/index.js file, and write the following:

  1. Import useEffect from the React library.

  2. Import NativeModules from the react-native package.

  3. Pull FRAuthSampleBridge from the NativeModules object.

  4. Write an async function within the useEffect callback to call the SDK start() method.

- import React from 'react';
+ import React, { useEffect } from 'react';
+ import { NativeModules } from 'react-native';
  import { SafeAreaProvider } from 'react-native-safe-area-context';

  import Theme from './theme/index';
  import { AppContext, useGlobalStateMgmt } from './global-state';
  import Router from './router';

+ const { FRAuthSampleBridge } = NativeModules;

  export default function App() {
    const stateMgmt = useGlobalStateMgmt({});

+   useEffect(() => {
+     async function start() {
+       await FRAuthSampleBridge.start();
+     }
+     start();
+   }, []);

    return (
      <Theme>
        <AppContext.Provider value={stateMgmt}>
          <SafeAreaProvider>
            <Router />
          </SafeAreaProvider>
        </AppContext.Provider>
      </Theme>
    );
  }

FRAuthSampleBridge is the JavaScript representation of the Swift bridge code we developed earlier. Any public methods added to the Swift class within the bridge code are available in the FRAuthSampleBridge object.

It’s important to initialize the SDK at a root level. Call this initialization step, so it resolves before any other native SDK methods can be used.

Step 2. Build the login view

Navigate to the app’s login view within the Simulator. You should see a "loading" spinner and a message that’s persistent, since the app doesn’t have the data needed to render the form. To ensure the correct form is rendered, the initial data needs to be retrieved from the ForgeRock server. That will be the first task.

react native session loading
Figure 7. Login screen with spinner

Since most of the action is taking place in src/components/journey/form.js, open it and add the following:

  1. Import FRStep from the @forgerock/javascript-sdk for improved ergonomics for handling callbacks.

  2. Import NativeModules from the react-native package.

  3. Pull FRAuthSampleBridge from the NativeModules object.

+ import { FRStep } from '@forgerock/javascript-sdk';
  import React from 'react';
+ import { NativeModules } from 'react-native';

  import Loading from '../utilities/loading';

+ const { FRAuthSampleBridge } = NativeModules;

@@ collapsed @@

To develop the login functionality, we first need to use the login() method from the bridge code to get the first set of callbacks, and then render the form appropriately. This login() method is an asynchronous method, so import a few additional packages from React to encapsulate this "side effect". Let’s get started!

Import two new modules from React: useState and useEffect. The useState() method is for managing the data received from the ForgeRock server, and the useEffect is for the FRAuthSampleBridge.login() method’s asynchronous, network request.

Compose the data gathering process using the following:

  1. Import useEffect from the React library.

  2. Write the useEffect function inside the component function.

  3. Write an async function within the useEffect for calling login.

  4. Write an async logout function to ensure user if fully logged out before attempting to login.

  5. Call FRAuthSampleBridge.login() to initiate the call to the login journey/tree.

  6. When the login() call returns with the data, parse the JSON string.

  7. Assign that data to our component state via the setState() method.

  8. Lastly, call this new method to execute this process.

  import { FRStep } from '@forgerock/javascript-sdk';
- import React from 'react';
+ import React, { useEffect, useState } from 'react';
  import { NativeModules } from 'react-native';

  import Loading from '../utilities/loading';

  const { FRAuthSampleBridge } = NativeModules;

  export default function Form() {
+   const [step, setStep] = useState(null);
+   console.log(step);
+
+   useEffect(() => {
+     async function getStep() {
+       try {
+         await FRAuthSampleBridge.logout();
+         const dataString = await FRAuthSampleBridge.login();
+         const data = JSON.parse(dataString);
+         const initialStep = new FRStep(data);
+         setStep(initialStep);
+       } catch (err) {
+         setStep({
+           type: 'LoginFailure',
+           message: 'Application state has an error.',
+         });
+       }
+     }
+     getStep();
+   }, []);

    return <Loading message="Checking your session ..." />;
  }
We are passing an empty array as the second argument into useEffect. This instructs the useEffect to only run once after the component mounts. This is functionally is equivalent to a class component using componentDidMount to run an asynchronous method after the component mounts.

The above code will result in two logs to your console:

  1. null

  2. An object with a few properties.

The property to focus on is the callbacks property. This property contains the instructions for what needs to be rendered to the user for input collection.

Import the components from NativeBase as well as the custom, local components within this journey/ directory:

  import { FRStep } from '@forgerock/javascript-sdk';
+ import { Box, Button, FormControl, ScrollView } from 'native-base';
  import React, { useEffect, useState } from 'react';
  import { NativeModules } from 'react-native';

  import Loading from '../utilities/loading';
+ import Alert from '../utilities/alert';
+ import Password from './password';
+ import Text from './text';
+ import Unknown from './unknown';

@@ collapsed @@

Now, within the Form function body, create the function that maps these imported components to their appropriate callbacks.

@@ collapsed @@

  export default function Form() {
    const [step, setStep] = useState(null);
    console.log(step);

@@ collapsed @@

+   function mapCallbacksToComponents(cb, idx) {
+     const name = cb?.payload?.input?.[0].name;
+     switch (cb.getType()) {
+       case 'NameCallback':
+         return <Text callback={cb} inputName={name} key="username" />;
+       case 'PasswordCallback':
+         return <Password callback={cb} inputName={name} key="password" />;
+       default:
+         // If current callback is not supported, render a warning message
+         return <Unknown callback={cb} key={`unknown-${idx}`} />;
+     }
+   }

    return <Loading message="Checking your session ..." />;
  }

Finally, return the appropriate component for the following states:

  • If there is no step data, render the Loading component to indicate the request is still processing.

  • If there is step data, and it is of type 'Step', then map over step.callbacks with the function from above.

  • If there is step data, but the type is 'LoginSuccess' or 'LoginFailure', render an alert.

@@ collapsed @@

+ if (!step) {
    return <Loading message='Checking your session ...' />;
+ } else if (step.type === 'Step') {
+   return (
+     <ScrollView>
+       <Box safeArea flex={1} p={2} w="90%" mx="auto">
+         <FormControl>
+           {step.callbacks?.map(mapCallbacksToComponents)}
+           <Button>Sign In</Button>
+         </FormControl>
+       </Box>
+     </ScrollView>
+   );
+ } else {
+   // Handle success or failure of the journey/tree
+   return (
+     <Box safeArea flex={1} p={2} w="90%" mx="auto">
+       <Alert message={step.message} type={step.type} />
+     </Box>
+   );
+ }

Refresh the page, and you should now have a dynamic form that reacts to the callbacks returned from our initial call to ForgeRock.

react native login form
Figure 8. Login screen form

Step 3. Handle the login form submission

Since a form that can’t submit anything isn’t very useful, we’ll now handle the submission of the user input values to ForgeRock. First, add a second useState to track whether the user is authenticated or not, and then edit the current Button element, adding an onPress handler with a simple, inline function. This function should do the following:

  1. Submit the modified step data to ForgeRock with the FRAuthSampleBridge.next() method.

  2. Test if the response property type has the value of 'LoginSuccess'.

  3. If successful, parse the response JSON.

  4. Call setStep() with the new object parsed from the JSON (this is mostly just for logging the step to the console).

  5. Call setAuthentication() to true, which is a global state method that triggers the app to react (pun intended!) to the new user state.

  6. Handle errors with a generic failure message.

@@ collapsed @@

  export default function Form() {
    const [step, setStep] = useState(null);
+   const [isAuthenticated, setAuthentication] = useState(false);
    console.log(step);

@@ collapsed @@

    return (
      <ScrollView>
        <Box safeArea flex={1} p={2} w="90%" mx="auto">
          <FormControl>
            {step.callbacks?.map(mapCallbacksToComponents)}
-         <Button>Sign In</Button>
+         <Button
+           onPress={() => {
+             async function getNextStep() {
+               try {
+                 const response = await FRAuthSampleBridge.next(
+                   JSON.stringify(step.payload),
+                 );
+                 if (response.type === 'LoginSuccess') {
+                   const accessInfo = JSON.parse(response.accessInfo);
+                   setStep({
+                     accessInfo,
+                     message: 'Successfully logged in.',
+                     type: 'LoginSuccess',
+                   });
+                   setAuthentication(true);
+                 } else {
+                   setStep({
+                     message: 'There has been a login failure.',
+                     type: 'LoginFailure',
+                   });
+                 }
+               } catch (err) {
+                 console.error(`Error: form submission; ${err}`);
+               }
+             }
+             getNextStep();
+           }}
+         >
+           Sign In
+         </Button>
        </FormControl>
      </Box>
    </ScrollView>
  );

@@ collapsed @@

After the app refreshes, use the test user to login. If successful, you should see a success message. Congratulations, you are now able to authenticate users!

react native login success
Figure 9. Login screen with successful authentication

What’s more, you can verify the authentication details by going to the Xcode or Safari log and observing the result of the last call to the ForgeRock server. It should have a type of "LoginSuccess" along with token information.

react native xcode login response
Figure 10. Successful login response from Xcode
If you got a login failure, you can re-attempt the login by going to the Device menu on the Simulator and selecting "Shake". This will allow you to reload the app, providing a fresh login form.

Handle the user provided values

You may ask, "How did the user’s input values get added to the step object?" Let’s take a look at the component for rendering the username input. Open up the Text component: components/journey/text.js. Notice how special methods are being used on the callback object. These are provided as convenience methods by the JavaScript SDK for getting and setting data.

@@ collapsed @@

  export default function Text({ callback }) {

@@ collapsed @@

    const error = handleFailedPolicies(
      callback.getFailedPolicies ? callback.getFailedPolicies() : [],
    );
    const isRequired = callback.isRequired ? callback.isRequired() : false;
    const label = callback.getPrompt();
    const setText = (text) => callback.setInputValue(text);
    return (
      <FormControl isRequired={isRequired} isInvalid={error}>
        <FormControl.Label mb={0}>{label}</FormControl.Label>
        <Input
          autoCapitalize="none"
          autoComplete="off"
          autoCorrect={false}
          onChangeText={setText}
          size="lg"
          type="text"
        />
        <FormControl.ErrorMessage>
          {error.length ? error : ''}
        </FormControl.ErrorMessage>
      </FormControl>
    );
  }

There are two important items to focus on

  • callback.getPrompt(): Retrieves the input’s label to be used in the UI.

  • callback.setInputValue(): Sets the user’s input on the callback while they are typing (i.e. onChangeText).

Since the callback is passed from the Form to the components by "reference" (not by "value"), any mutation of the callback object within the Text (or Password) component is also contained in the step object in the Form component.

You may think, "That’s not very idiomatic React! Shared, mutable state is bad." And, yes, you are correct, but we are taking advantage of this to keep everything simple (and this guide from being too long), so I hope you can excuse the pattern.

Each callback type has its own collection of methods for getting and setting data in addition to a base set of generic callback methods. The SDK automatically adds these methods to the callback’s prototype. For more information about these callback methods, see our API documentation, or the source code in GitHub, for more details.

Step 4. Request user info and redirecting to home screen

Now that the user can login, let’s go one step further and request information about the authenticated user to display their name and other information. We will now utilize the existing FRAuthSampleBridge.getUserInfo() method already included in the bridge code.

Let’s do a little setup before we make the request to the ForgeRock server:

  1. Add useContext to the import from React so that we have access to the global state.

  2. Import AppContext from the global-state module.

  3. Call useContext with our AppContext to provide access to the setter methods.

  4. Add one more useEffect function to detect the change of the user’s authentication.

  import { FRStep } from '@forgerock/javascript-sdk';
  import { Box, Button, FormControl, ScrollView } from 'native-base';
- import React, { useEffect, useState } from 'react';
+ import React, { useContext, useEffect, useState } from 'react';
  import { NativeModules } from 'react-native';

+ import { AppContext } from '../../global-state';
@@ collapsed @@

  export default function Form() {
+   const [_, methods] = useContext(AppContext);
    const [step, setStep] = useState(null);
    const [isAuthenticated, setAuthentication] = useState(false);
    console.log(step);

    useEffect(() => {
      async function getStep() {
        try {
          await FRAuthSampleBridge.logout();
          const dataString = await FRAuthSampleBridge.login();
          const data = JSON.parse(dataString);
          const initialStep = new FRStep(data);
          setStep(initialStep);
        } catch (err) {
          console.error(`Error: request for initial step; ${err}`);
        }
      }
      getStep();
    }, []);

+   useEffect(() => {
+
+   }, [isAuthenticated]);

@@ collapsed @@

It’s worth noting that the isAuthenticated declared in the array communicates to React that this useEffect should only execute if the state of that variable changes. This prevents unnecessary code execution since the value is initially false, and continues to be false until the user completes authentication.

With the setup complete, implement the request to ForgeRock for the user’s information. Within this empty useEffect, add an async function to make that call to FRAuthSampleBridge.getUserInfo() and call it only when isAuthenticated is true.

@@ collapsed @@

    useEffect(() => {
+     async function getUserInfo() {
+       const userInfo = await FRAuthSampleBridge.getUserInfo();
+       console.log(userInfo);
+
+       methods.setName(userInfo.name);
+       methods.setEmail(userInfo.email);
+       methods.setAuthentication(true);
+     }
+
+     if (isAuthenticated) {
+       getUserInfo();
+     }
    }, [isAuthenticated]);

@@ collapsed @@

In the code above, we collected the user information and set a few values to the global state to allow the app to react to this information. In addition to updating the global state, the React Navigation also reacts to the global state change and renders the new screens and tab navigation.

When you test this in the Simulator, completing a successful authentication results in the home screen being rendered with a success message. The user’s name and email are included for visual validation. You can also view the console in Safari and see the user’s information logged.

react native home screen success
Figure 11. Home screen after successful authentication

Add logout functionality to our bridge and React Native code

Clicking the Sign Out button within the navigation results in the logout page rendering with a persistent "loading" spinner and message. This is due to the missing logic that we’ll add now.

react native logging out
Figure 12. Logout screen with spinner

Handle logout request in React Native

Now, add the logic into the view to call this new Swift method. Open up the screens/logout.js file and import the following:

  1. useEffect and useContext from React

  2. useHistory from React Router

  3. AppContext from the global state module

- import React from 'react';
+ import React, { useContext, useEffect } from 'react';
+ import { NativeModules } from 'react-native';

+ import { AppContext } from '../global-state';
  import { Loading } from '../components/utilities/loading';

+ const { FRAuthSampleBridge } = NativeModules;

@@ collapsed @@

Since logging out requires an async, network request, we need to wrap it in a useEffect and pass in a callback function with the following functionality:

@@ collapsed @@

  export default function Logout() {
+   const [_, { setAuthentication }] = useContext(AppContext);
+
+   useEffect(() => {
+     async function logoutUser() {
+       try {
+         await FRAuthSampleBridge.logout();
+       } catch (err) {
+         console.error(`Error: logout; ${err}`);
+       }
+       setAuthentication(false);
+     }
+     logoutUser();
+   }, []);
+
    return <Loading message="You're being logged out ..." />;
  }

Since we only want to call this method once, after the component mounts, we will pass in an empty array as a second argument for useEffect(). The use of the setAuthentication() method empties or falsifies the global state to clean up and re-renders the home screen.

Revisit the app within the Simulator, and tap the Sign Out button. You should see a quick flash of the loading screen, and then the home screen should be displayed with the logged out UI state.

react native home screen
Figure 13. Logged out home screen

You should now be able to successfully authenticate a user, display the user’s information, and log a user out. Congratulations, you just built a ForgeRock protected iOS app with React Native.

In part two of this series, we explore how to write the bridge code for Android, while utilizing the existing React Native UIs written here.

Copyright © 2010-2022 ForgeRock, all rights reserved.