Build a Protected Flutter Mobile App: Part One – iOS

This was originally published as an SDK blog

This is part one of a multi-part series covering the basics of developing a ForgeRock-protected, mobile app with Flutter. This part focuses on developing the iOS bridge code along with a minimal Flutter 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 Flutter 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). Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase. Flutter requires this bridging code to provide the Hybrid (Dart) layer access to native (Swift in this case) APIs or dependencies.

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

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

  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 Flutter app

How to architect or construct Flutter apps is outside of the scope of this guide. It’s also worth noting that there are many Flutter 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 some basic Flutter patterns and the Material design UI library.

Using 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 or Access Management. If you don’t have access to either, and you are interested in ForgeRock’s Identity and Access Management 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. Just curious about the bridge code and Flutter implementation details: skip to Develop the iOS bridge code for the ForgeRock SDK

Requirements

Prerequisites

  • Xcode, the iOS Simulator and related tools.
  • Swift SPM (Swift Package Manager).
  • Dart and the flutter ecosystem of modules (Get the Dart SDK | Dart).
  • The command line interface (e.g. Terminal, Shell or Bash).
  • Core Git commands (e.g. clone, checkout).
  • Android Studio for developing Flutter applications.
  • Xcode 12 or higher with the developer tools installed.
  • Admin access to an instance of ForgeRock Identity Cloud or Access Management (AM).
  • Latest Flutter release (Install | Flutter)
  • A tool or service to generate a security certificate and key (self-signed is fine).
  • Latest Android studio with the Flutter plug-in installed. (Set up an editor | Flutter)
  • Set the Dart SDK path in Android Studio. (You can find that in the folder that you have dowloaded the Flutter SDK. (ex: ~/flutter/bin/cache/dart-sdk)

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.

Screenshot of username-password journey in ID Cloud.

check_circle_outline TIP
For more information about building journeys/trees, visit our ForgeRock documentation for tree configuration on AM or for journey configuration on ID 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: FlutterOAuthClient
  • 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.flutter.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.

Screenshot of an OAuth client configuration in ID Cloud.

check_circle_outline TIP
For more information about configuring OAuth clients, visit our ForgeRock documentation for OAuth 2.0 client configuration on AM or for OAuth client/application configuration on ID Cloud.

Step 3. Create a test user

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

If using ForgeRock ID Cloud, follow our ForgeRock documentation for creating a user in ID Cloud.

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

  1. Click on 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.

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

Local project setup

Step 1. Clone the project

First, create a new directory mkdir, clone the Flutter Sample project to your local computer and checkout the branch for this guide:

mkdir forgerock-flutter-sample
cd forgerock-flutter-sample
git clone https://github.com/ForgeRock/forgerock-flutter-sample.git
git checkout part-one-start

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

create NOTE
The branch used in this guide is an overly-simplified version of the sample app found in the main Git branch within the flutter-todo directory. This “full version” is a to-do application that communicates with a protected REST API.

Screenshot of the to-do screen from the full sample app.

Step 2. Install the project dependencies

This Flutter app requires the installation of the Native ForgeRock iOS SDK. You can install this by using SPM (Swift Package Manager) on the generated iOS project.

  1. Navigate to the iOS project. (forgerock-flutter-sample/Flutter_To_Do_app/flutter_todo_app/ios)
  2. Use XCode to open “Runner.xcworkspace”.
  3. In XCode, select the Runner project and navigate to Package Dependencies.
  4. Click on the (+) sign and add the Forgerock iOS SDK repository. (GitHub - ForgeRock/forgerock-ios-sdk: ForgeRock Mobile iOS SDK)
  5. Add in the project the FRCore and FRAuth libraries.

Next, we need to open Android Studio and build the project. If you haven’t configured Android studio for Flutter please follow the guide in the Flutter documentation (Set up an editor | Flutter).

Don’t forget to set the Dart SDK path in Android studio. You can find that in the folder that you have dowloaded the Flutter SDK. (ex: ~/flutter/bin/cache/dart-sdk)

In Android Studio, click “File->Open” and navigate to “forgerock-flutter-sample/Flutter_To_Do_app/”. When Android studio loads the project and is ready, install any gradle dependencies and select the iOS simulator to build and run the project.

Additionally, you are required to install the node dependencies for the ToDo node Server.

  1. Open up a Terminal window and navigate to the root folder of your project.
  2. Run the “npm install” command.

Step 3. Edit the .env.js file

Using the ForgeRock server settings from above, edit the .env.js file within the project. This can be found the root folder of your project. Add your relevant values to configure all the important server settings to your project. Not all variables will need values at this time. You can list the file in the Terminal by doing ls -a and edit it using a Text editor like nano or vi.

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_ ForgeRock AM URL
 const DEBUGGER_OFF = true;
 const DEVELOPMENT = true;
 const API_URL = 'https://api.example.com:9443'; // (your resource API server's URL)
 const JOURNEY_LOGIN = 'Login'; // (name of journey/tree for Login)
 const JOURNEY_REGISTER = 'Registration'; // (name of journey/tree for Register)
 const SEC_KEY_FILE = './updatedCerts/api.example.com.key';
 const SEC_CERT_FILE = './updatedCerts/api.example.com.crt';
 const REALM_PATH = ''; //Required (ex: alpha)
 const REST_OAUTH_CLIENT = ''; // (name of private OAuth 2.0 client/application)
 const REST_OAUTH_SECRET = ''; // (the secret for the private OAuth 2.0 client/application)
 const WEB_OAUTH_CLIENT = ''; // (the name of the public OAuth 2.0 client/application)
 const PORT = '9443';

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 we will use it 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: We will use these values in a future part of this series, so any string value will do.

Build and run the project

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

  1. Go back to the iOS project. (forgerock-flutter-sample/Flutter_To_Do_app/flutter_todo_app/ios)
  2. If the project is not already open in XCode double click on the “Runner.xcworkspace”.
  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 Flutter 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. Under the “General” tab, make sure that the FRAuth and FRCore frameworks are added to your Target’s “Frameworks, Libraries, and Embedded Content”.
  2. Bridge code has been altered, so be aware of API name changes.

Using Xcode and iOS Simulator

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 Flutter layer.

Screenshot of the home screen from the starter code.

create NOTE
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.

Screenshot of Xcode console output.

Implement the iOS bridge code for authentication

Step 1. Review the iOS related files

We first need to review the files that allow for the “bridging” between our Flutter project and our native SDKs. 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 iOS SDK to the appropriate ForgeRock server.

create NOTE
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.

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/Runner 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>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</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 ForgeRock 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 ForgeRock 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 Runner directory, find the FRAuthSampleBridge file and open it. We have some 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 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 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 login the user. This initial call to the ForgeRock 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 iOS SDK 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.

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

Step 6. 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 @@

Building a Flutter form for simple login

Step 1. Initializing the SDK

First, let’s review how the application renders the home view. Open Android Studio and navigate to the Flutter project.

flutter_todo_app > java/main.dart

Open up the second file in the above sequence, the java/main.dart file, and notice the following:

  1. The use of import 'package:flutter/material.dart'; from the Flutter library.
  2. The TodoApp class extending StatefulWidget
  3. The _TodoAppState class extending State<TodoApp>
  4. Building the UI for the the Navigation bar
import 'package:flutter/material.dart';
import 'package:flutter_todo_app/home.dart';
import 'package:flutter_todo_app/login.dart';
import 'package:flutter_todo_app/todolist.dart';

void main() => runApp(
  new TodoApp(),
);

class TodoApp extends StatefulWidget {
  @override
  _TodoAppState createState() => new _TodoAppState();
}

class _TodoAppState extends State<TodoApp> {
  int _selectedIndex = 0;

  final _pageOptions = [
    HomePage(),
    LoginPage(),
    TodoList(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: Scaffold(
        body: _pageOptions[_selectedIndex],
        bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.vpn_key),
              label: 'Sign In',
            ),
          ],
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.blueAccent[800],
          onTap: _onItemTapped,
          backgroundColor: Colors.grey[200],
        )
      ),
    );
  }
}

Flutter uses something called MethodChannel to communicate between Flutter and the Native layer. In this application we will define a MethodChannel with the following identifier: 'forgerock.com/SampleBridge'.

The same identifier will be used in the iOS FRSampleBridge so that the two layers communicate and pass information. To initialize the ForgeRock SDK when the log in view first loads, we call the frStart method on the bridge code.

create NOTE
It’s important to initialize the SDK as early as possible. Call this initialization step so it resolves before any other native SDK methods can be used.

Step 2. Building the login view

Navigate to the app’s login view within the Simulator. You should see an empty screen with a button, since the app doesn’t have the data needed to render the form. To render the correct form, retrieve the initial data from the ForgeRock server. This is our first task.

Since most of the action is taking place in flutter_todo_app/Java/login.dart, open it and add the following:

  1. Import FRNode.dart from the the Dart helper classes provided for improved ergonomics for handling callbacks. import 'package:flutter_todo_app/FRNode.dart';
  2. If not already there, import async, convert, scheduler, services from the flutter package. Add the following: import 'dart:async';, import 'dart:convert';, import 'package:flutter/scheduler.dart';, import 'package:flutter/services.dart';,
  3. Create a static reference for the method channel MethodChannel('forgerock.com/SampleBridge').
  4. Override the initState Flutter lifecycle method and initialize the SDK.
class _LoginPageState extends State<LoginPage> {
+  static const platform = MethodChannel('forgerock.com/SampleBridge'); //Method channel as defined in the native Bridge code

  @@ collapsed @@

 //Lifecycle Methods
+  @override
+  void initState() {
+    super.initState();
+    SchedulerBinding.instance?.addPostFrameCallback((_) => {
+      //After creating the first controller that uses the SDK, call the 'frAuthStart' method to initialize the native SDKs.
+      _startSDK()
+    });
+  }

 // SDK Calls -  Note the promise type responses. Handle errors on the UI layer as required
   Future<void> _startSDK() async {
+    String response;
+    try {
+
+      //Start the SDK. Call the frAuthStart channel method to initialise the native SDKs
+      final String result = await platform.invokeMethod('frAuthStart');
+      response = 'SDK Started';
+      _login();
+    } on PlatformException catch (e) {
+      response = "SDK Start Failed: '${e.message}'.";
+    }
  }

@@ 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. Let’s get started!

Compose the data gathering process using the following:

  1. After the SDK initialization is complete, call the _login() method.
  2. Use the platform reference to call the Bridge login method platform.invokeMethod('login').
  3. Parse the response and call _handleNode() method.
  4. Handle any errors that might be returned from the Bridge.
  @@ collapsed @@

  Future<void> _login() async {
+    try {
+      //Call the default login tree.
+      final String result = await platform.invokeMethod('login');
+      Map<String, dynamic> frNodeMap = jsonDecode(result);
+      var frNode = FRNode.fromJson(frNodeMap);
+      currentNode = frNode;
+
+      //Upon completion, a node with callbacks will be returned, handle that node and present the callbacks to UI as needed.
+      _handleNode(frNode);
+    } on PlatformException catch (e) {
+      debugPrint('SDK Error: $e');
+      Navigator.pop(context);
+    }
  }

The above code is expected to return either a Node with a set of Callbacks, or a Success/Error message. We need to handle any exceptions thrown from the bridge on the catch block. Typically, when we begin the Authentication Journey/Tree, this returns a Node. Using the FRNode helper object, we parse the result in a native Flutter FRNode object.

In the next step we are going to “handle” this node, and produce our UI.

  @@ collapsed @@
  // Handling methods
  void _handleNode(FRNode frNode) {
+    // Go through the node callbacks and present the UI fields as needed. To determine the required UI element, check the callback type.
+    frNode.callbacks.forEach((frCallback) {
+      final controller = TextEditingController();
+      final field = TextField(
+        controller: controller,
+        obscureText: frCallback.type == "PasswordCallback", // If the callback type is 'PasswordCallback', make this a 'secure' textField.
+        enableSuggestions: false,
+        autocorrect: false,
+        decoration: InputDecoration(
+          border: OutlineInputBorder(),
+          labelText: frCallback.output[0].value,
+        ),
+      );
+      setState(() {
+        _controllers.add(controller);
+        _fields.add(field);
+      });
+    });
  }

The _handleNode method focuses on the callbacks property. This property contains instructions about what to render to collect user input.

The previous code processes the Node callbacks and generates two TextFields: A TextField for the username. A TextField for the password. Use the frCallback.type to differentiate between the two TextFields and obscure the text of each TextField. Next, We add the TextFields to our List and create the accompanying TextEditingControllers.

Run the app again, and you should see a dynamic form that reacts to the callbacks returned from our initial call to ForgeRock.

Screenshot of login screen with rendered form

Step 3. Handling 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. Continuing in login.dart, edit the current _okButton element adding an onPressed handler calling the _next() function. This function should do the following:

  1. Go through the _controllers array to capture the values of the form elements.
  2. Update the Node callbacks with those values.
  3. Submit the results to ForgeRock.
  4. Check the response for a LoginSuccess message or if a new node is returned, handle this in a similar way and resubmit the user inputs as needed.
  5. Handle errors with a generic failure message.
@@ collapsed @@

Widget _okButton() {
  return Container(
    color: Colors.transparent,
    width: MediaQuery.of(context).size.width,
    margin: EdgeInsets.all(15.0),
    height: 60,
    child: TextButton(
      style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.blue)),
      onPressed: () async {
        showAlertDialog(context);
+       _next();
      },
      child:
      Text(
        "Sign in",
        style: TextStyle(color: Colors.white),
      ),
    ),
  );
}

@@ collapsed @@

 Future<void> _next() async {
   // Capture the User Inputs from the UI, populate the currentNode callbacks and submit back to AM
+  currentNode.callbacks.asMap().forEach((index, frCallback) {
+    _controllers.asMap().forEach((controllerIndex, controller) {
+      if (controllerIndex == index) {
+        frCallback.input[0].value = controller.text;
+      }
+    });
+  });
+  String jsonResponse = jsonEncode(currentNode);
+  try {
+    // Call the SDK next method, to submit the User Inputs to AM. This will return the next Node or a Success/Failure
+    String result = await platform.invokeMethod('next', jsonResponse);
+    Navigator.pop(context);
+    Map<String, dynamic> response = jsonDecode(result);
+    if (response["type"] == "LoginSuccess") {
+      _navigateToNextScreen(context);
+    } else  {
+      //If a new node is returned, handle this in a similar way and resubmit the user inputs as needed.
+      Map<String, dynamic> frNodeMap = jsonDecode(result);
+      var frNode = FRNode.fromJson(frNodeMap);
+      currentNode = frNode;
+      _handleNode(frNode);
+    }
+  } catch (e) {
+    Navigator.pop(context);
+    debugPrint('SDK Error: $e');
+  }
 }

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

Screenshot of login screen with success alert.

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

Screenshot of successful login response in Xcode’s output.

Handling the user provided values

You may ask, “How did the user’s input values get added to the Node object?” Let’s take a look at the component for handling the user input submission. Notice how we loop through the Node Callbacks and the _controllers array. Each input is set on the frCallback.input[0].value and then we call FRSampleBridge next method.

@@ collapsed @@

// Capture the User Inputs from the UI, populate the currentNode callbacks and submit back to AM
  currentNode.callbacks.asMap().forEach((index, frCallback) {
    _controllers.asMap().forEach((controllerIndex, controller) {
      if (controllerIndex == index) {
        frCallback.input[0].value = controller.text;
      }
    });
  });

  String jsonResponse = jsonEncode(currentNode);

@@ collapsed @@

  try {
    // Call the SDK next method, to submit the User Inputs to AM. This will return the next Node or a Success/Failure
    String result = await platform.invokeMethod('next', jsonResponse);

    @@ collapsed @@

  } catch (e) {
    Navigator.pop(context);
    debugPrint('SDK Error: $e');
  }

There are two important items to focus on regarding the FRCallback object.

  • callback.type: Retrieves the call back type so that can Identity how to present the callback in the UI.
  • callback.input: The input array. This array contains the inputs that you need to set the values for.

Since the NameCallback and PasswordCallback only have one input, you can set the value of them by calling frCallback.input[0].value = controller.text;. Some other callbacks might contain multiple inputs, so some extra code will be required to set the values of those.

Each callback type has its own collection of inputs and outputs. Those are exposed as arrays that the developer can loop through and act upon. Many callbacks have common base objects in iOS and Android, like the the SingleValueCallback, but appear as different types NameCallback or PasswordCallback to allow for easier differentiation in the UI layer. You can find a full list of the supported callbacks of the SDKs here.

Step 4. Redirecting to the TodoList screen and requesting user info

Now that the user can login, let’s go one step further and redirect to the TodoList screen. After we get the LoginSccess message we can call the _navigateToNextScreen() method. This will navigate to the TodoList class. When the TodoList initializes, we want to 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. Override the initState method in the _TodoListState class in the todolist.dart.
  2. Create a SchedulerBinding.instance?.addPostFrameCallback to excecute some code when the state is loaded.
  3. Call _getUserInfo().
@@ collapsed @@
//Lifecycle methods

+ @override
+ void initState() {
+   super.initState();
+   SchedulerBinding.instance?.addPostFrameCallback((_) => {
+     //Calling the userinfo endpoint is going to give use some user profile information to enrich our UI. Additionally, verifies that we have a valid access token.
+     _getUserInfo()
+   });
+ }

@@ collapsed @@

With the setup complete, implement the request to ForgeRock for the user’s information. Within this empty _getUserInfo(), add an async function to make that call to FRAuthSampleBridge.getUserInfo and parse the response.

@@ collapsed @@

Future<void> _getUserInfo() async {
  showAlertDialog(context);
  String response;
+  try {
+    final String result = await platform.invokeMethod('getUserInfo');
+    Map<String, dynamic> userInfoMap = jsonDecode(result);
+    response = result;
+    header = userInfoMap["name"];
+    subtitle = userInfoMap["email"];
+    Navigator.pop(context);
+    setState(() {
+      _getTodos();
+    });
+  } on PlatformException catch (e) {
+    response = "SDK Start Failed: '${e.message}'.";
+    Navigator.pop(context);
+  }
+  debugPrint('SDK: $response');
}

@@ collapsed @@

In the code above, we collected the user information and set the name and email of the user in some variables. In addition to updating the user info, we will call the _getTodos() method in order to retrieve ToDos from the server. Notice that we use the setState() function. TThis ensures that our UI is updated based on the newly received information.

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 XCode and see more complete logs.

Screenshot of home screen with success alert.

Adding logout functionality to our bridge and Flutter code

Clicking on the “Sign Out” button, results in creating and rendering an alert view asking you if you are sure you want to log out with 2 options (yes/no). Clicking ‘yes’ does nothing at the moment. We will now implement that missing logic.

Handle logout request in Flutter

Now, add the logic into the view to call this new Swift method. Open up the todolist.dart file and add the following:

@@ collapsed @@

  TextButton(
    child: const Text('Yes'),
+        onPressed: () {
+            Navigator.of(context).pop();
+            _logout();
+        },
  ),

@@ collapsed @@

  Future<void> _logout() async {
+    final String result = await platform.invokeMethod('logout');
+    _navigateToNextScreen(context);
  }

Revisit the app within the Simulator, and tap the “Sign Out” button. This time around when clicking yes, will dispose of the alert and log you out, returning you back to the log in screen. If you tap no, you will return back to the ToDo list 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 Flutter.

In part two of this series, we explore how to write the bridge code for Android, while utilizing the existing Flutter UI build in this blog.

Helpful Links

Documentation

Knowledge Base