DEV Community

Cover image for VPN on demand and Kerberos SSO in Flutter mobile app
Aleksander Parchomenko
Aleksander Parchomenko

Posted on

VPN on demand and Kerberos SSO in Flutter mobile app

Intro

Recently I have a pleasure to implement mobile app that among others authorizations uses Kerberos SSO. The app is distributed using corporate MDM with VPN connection configured there. Google Flutter framework was used as underlying tech stack for it. We faced known old Flutter issue https://github.com/flutter/flutter/issues/41500 for establishing VPN on demand on iOS when app tries to make a calls to internal corporate API. There a number of suggestions in the issue how to solve the problem, but we've decided to do it in another way.

Solution

It was helpful information on the relatively old apple dev thread https://developer.apple.com/forums/thread/76448 about VPN on demand:

BSD Sockets does not have a connect by name API. If you need to use BSD Sockets for other reasons — for example, you’re porting some cross-platform code — you can do some sneaky stuff to connect by name and then continue using BSD Sockets for the bulk of your I/O.

That was a hint for us to use something like ping call with native part of code that can set up the connection. For web and logged in in Active Directory users we already have logon endpoint that does some checks for AD user in the application and returns JWT token to establish Bearer authorization for the API.

The idea is to use native SWIFT code to make requests to logon endpoint. For the early first request VPN wasn't set and when we've tried to call logon API timeout has been appeared. The solution is empty ping endpoint (or some lightweight healthcheck) just to be called during first application start before logon process: that sets up VPN and only after that use logon and other API calls using Flutter technique that you use in application.

Native code

To call platform-specific code we used platform channels, where the Flutter portion of the app sends messages to its host (the non-Dart portion of the app) over a platform channel, the host listens on the platform channel, and receives the message. It then calls into any number of platform-specific APIs—using the native programming language—and sends a response back to the client, the Flutter portion of the app.
The piece of code for it (lets call it repository - an abstraction layer for any type of data persistence: local or remote via API) which sends a message to the host:

class MaintenanceRepository {
  static const platform = MethodChannel('com.flutter/http');

  MaintenanceRepository();

  Future<int> ping() async {
    try {

      if (!Platform.isIOS) {
        return HttpStatus.ok;
      }

      final response = await platform.invokeMethod('http-get-ping', {"url": "$apiUrl/api/configuration/vpn"});
      return response["statusCode"];
    } on PlatformException catch (_) {
      return HttpStatus.requestTimeout;
    } catch (e, stackTrace) {
      Log.error(e, stackTrace);
      return HttpStatus.requestTimeout;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

and native part of the code placed in ios/Runner/AppDelegate.swift with callback to the Flutter app:


@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    }

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
         let httpChannel = FlutterMethodChannel(name: "com.flutter/http", binaryMessenger: controller.binaryMessenger)
            httpChannel.setMethodCallHandler({
                (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                // Note: this method is invoked on the UI thread.
                guard call.method == "http-get-ping" else {
                    result(FlutterMethodNotImplemented)
                    return
                }

                let args = call.arguments as! NSDictionary
                let url = args["url"] as? String ?? ""

                if call.method == "http-get-ping" {
                    self.ping(url: url, result: result)
                }
            })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

    func ping(url: String, result: @escaping FlutterResult) {
        let session = URLSession.shared
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "GET"
        let task = session.dataTask(with: request) { data, response, error in

            if error != nil {
                result(FlutterError(code: "UNREACHABLE", message: "URL not reachable", details: nil))
                return
            }
            result(["statusCode": (response as? HTTPURLResponse)?.statusCode]);
        }
        task.resume()
    }
}
Enter fullscreen mode Exit fullscreen mode

The above stuff resolves the VPN on demand setup on iOS application written on Flutter.

No application wrapping or MDM SDK has been added to the application in this approach.

Latest comments (0)