Ping SDKs

Step 5. Implement the iOS bridge code

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

In Xcode, navigate to the Runner/Runner directory, and you will see a few important files:

FRAuthSampleBridge.swift

The main Swift bridging code that provides the callable methods for the Flutter 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 Ping SDK for iOS to the appropriate authorization server.

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

Configure your .plist file

In the Xcode directory/file list section, also known as the Project Navigator, complete the following:

  1. Find FRAuthConfig.plist file within the ios/Runner 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>e1babb394ea5130</string>
    <key>forgerock_enable_cookie</key>
    <true/>
    <key>forgerock_oauth_client_id</key>
    <string>flutterOAuthClient</string>
    <key>forgerock_oauth_redirect_uri</key>
    <string>https://com.example.flutter.todo/callback</string>;
    <key>forgerock_oauth_scope</key>
    <string>openid profile email address</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>com.forgerock.flutterTodoApp</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 an PingOne Advanced Identity Cloud tenant, you can find this random string value under the Tenant Settings in the top-right dropdown in the admin UI. If you have your own installation of PingAM, this is often iPlanetDirectoryPro.

forgerock_url and 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 Runner directory, find the FRAuthSampleBridge file and open it. We have parts of the file 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 Flutter

  public class FRAuthSampleBridge {
      var currentNode: Node?
      private let session = URLSession(configuration: .default)

   @objc func frAuthStart(result: @escaping FlutterResult) {

+     /**
+      * Set log level to all
+      */
+     FRLog.setLogLevel([.all])
+
+     do {
+       try FRAuth.start()
+       let initMessage = "SDK is initialized"
+       FRLog.i(initMessage)
+       result(initMessage)
+     } catch {
+       FRLog.e(error.localizedDescription)
+       result(FlutterError(code: "SDK Init Failed",
+                              message: error.localizedDescription,
+                              details: nil))
+     }
   }

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 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 frAuthStart(result: @escaping FlutterResult) {
    // Set log level according to your needs
    FRLog.setLogLevel([.all])

    do {
      try FRAuth.start()
        result("SDK Initialised")
        FRUser.currentUser?.logout()
    }
    catch {
      FRLog.e(error.localizedDescription)
        result(FlutterError(code: "SDK Init Failed",
                            message: error.localizedDescription,
                            details: nil))
    }
  }

 @objc func login(result: @escaping FlutterResult) {
+   FRUser.login { (user, node, error) in
+     self.handleNode(user, node, error, completion: result)
+   }
 }

@@ collapsed @@

This login() function initializes the journey/tree specified for authentication. You call this method without arguments as it does not log the user in. This initial call to the server will return the first set of callbacks that represents the first node in your journey/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 Flutter layer is limited to serialized objects. This method can be written in many ways and should be written in whatever way is best for your application.

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. In the bridge file, 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(result: @escaping FlutterResult) {
      FRUser.login { (user, node, error) in
          self.handleNode(user, node, error, completion: result)
      }
  }

 @objc func next(_ response: String, completion: @escaping FlutterResult) {
+   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))
+       completion(FlutterError(code: "Error",
+                                      message: error.localizedDescription,
+                                  details: nil))
+     }
+
+     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)
+           }
+         }
+       }
+     }
+
+     //node.next logic goes here
+
+
+   } else {
+     completion(FlutterError(code: "Error",
+                                message: "UnkownError",
+                                details: 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 logic goes here
+     node.next(completion: { (user: FRUser?, node, error) in
+       if let node = node {
+         self.handleNode(user, node, error, completion: completion)
+       } else {
+         if let error = error {
+           completion(FlutterError(code: "LoginFailure",
+                                          message: error.localizedDescription,
+                                      details: nil))
+           return
+         }
+
+         let encoder = JSONEncoder()
+         encoder.outputFormatting = .prettyPrinted
+         do {
+             if let user = user, let token = user.token, let data = try? encoder.encode(token), let jsonAccessToken = String(data: data, encoding: .utf8) {
+                 completion(try ["type": "LoginSuccess", "sessionToken": jsonAccessToken].toJson())
+             } else {
+                 completion(try ["type": "LoginSuccess", "sessionToken": ""].toJson())
+             }
+           }
+           catch {
+             completion(FlutterError(code: "Serializing Response failed",
+                                     message: error.localizedDescription,
+                                     details: nil))
+           }
+       }
+     })
    } else {
      completion(FlutterError(code: "Error",
                                message: "UnkownError",
                                details: 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 {
      completion(FlutterError(code: "Error",
                                message: "UnkownError",
                                details: nil))
    }

  @objc func frLogout(result: @escaping FlutterResult) {
+        FRUser.currentUser?.logout()
+        result("User logged out")
  }

@@ collapsed @@