Ping SDKs

Step 5. Implement the iOS bridge code

Review the files that allow for the "bridging" between the React Native project and the native Ping SDK.

Within Xcode, navigate to the forgerock-react-native-sample/reactnative-todo/ios 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.

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

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

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

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

  • reactnativetodo/FRAuthConfig.plist: The .plist file that configures the Ping SDK for iOS to the appropriate authorization 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.

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 PingOne Advanced Identity Cloud or PingAM cookie.

  3. Add the OAuth client you created from above.

  4. Add your authorization server 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>iPlanetDirectoryPro</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 PingOne Advanced 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 PingAM, this is often iPlanetDirectoryPro.

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

  • forgerock_realm: The realm of your 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.

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 Ping SDK for iOS’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.

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 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 Ping SDK for iOS 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 Ping SDK for JavaScript to convert this basic JSON of data into a decorated object for better ergonomics is used in this tutorial.

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 Ping SDK for iOS. 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.

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 @@