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:
-
Find
FRAuthConfig.plist
file within theios/reactnativetodos
directory. -
Add the name of your PingOne Advanced Identity Cloud or PingAM cookie.
-
Add the OAuth client you created from above.
-
Add your authorization server URLs.
-
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 ofteniPlanetDirectoryPro
. -
forgerock_url
&forgerock_oauth_url
: The URL of PingAM within your server installation. -
forgerock_realm
: The realm of your server (likelyroot
,alpha
orbeta
). -
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.