Oauth2 flow in Flutter app

On request of @Igor, I'll post the code we used to solve this. The idea is based both on the answer of @CollinJackson, and on how the AppAuth library does the same thing. Note: I don't have the iOS code here, but the code should be pretty trivial to anyone who regularly does iOS development.

Android-specific code

First, create a new Activity, and register it in the manifest to receive the URIs:

    <activity
        android:name=".UriReceiverActivity"
        android:parentActivityName=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="organization" android:host="login.oauth2" /> 
            <!-- Triggering URI would be organization://login.oauth2 -->
        </intent-filter>
    </activity>

In your Java-code, by default, there is one Activity (MainActivity). Start a new MethodChannel in this activity:

public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler {

  private static final String CHANNEL_NAME = "organization/oauth2";

  public static MethodChannel channel;

  @Override  
  protected void onCreate(Bundle savedInstanceState) {    
      super.onCreate(savedInstanceState);    
      GeneratedPluginRegistrant.registerWith(this);

      channel = new MethodChannel(getFlutterView(), CHANNEL_NAME);
      channel.setMethodCallHandler(this);
  }
}

Note that this code is incomplete, since we also handle calls from this. Just implemented this method, and the method calls you might add. For example, we launch Chrome custom tabs using this channel. However, to get keys back to Dart-land, this is not necessary (just implement the method).

Since the channel is public, we can call it in our UriReceiverActivity:

public class UriReceiverActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Uri data = getIntent().getData();
    Map<String, Object> map = new HashMap<>();
    map.put("URL", data.toString());
    MainActivity.channel.invokeMethod("onURL", map);

    // Now that all data has been sent back to Dart-land, we should re-open the Flutter
    // activity. Due to the manifest-setting of the MainActivity ("singleTop), only a single
    // instance will exist, popping the old one back up and destroying the preceding
    // activities on the backstack, such as the custom tab.
    // Flags taken from how the AppAuth-library accomplishes the same thing
    Intent mainIntent = new Intent(this, MainActivity.class);
    mainIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    startActivity(mainIntent);
    finish();
}
}

This is heavily inspired by this code.

Now, the Flutter app is re-opened, and the URL (with token) is sent back to Dart-land.

Flutter code

In Dart, we have a singleton listening in on the channel (I'll only post fragments of the code, since it's not that nice and quite scattered around the file):

  // Member declaration
  _platform = const MethodChannel('organization/oauth2');
  // Instantiation in constructor
  _platform.setMethodCallHandler(_handleMessages);
  // Actual message handler:
  void _handleMessages(MethodCall call) {
    switch (call.method) {
      case "onURL":
        // Do something nice using call.arguments["URL"]
    }
  }

On iOS, do the same as on Android, by sending the URL down the channel with that name and under the same command. The Dart code then doesn't need any changes.

As for launching the browser, we just use the url_launcher plugin. Note that we are not restricted to using a WebView, we can use any browser on the device.

Note that there are probably easier ways to do this, but since we had to make this quite early in Flutter's alpha, we couldn't look at other implementations. I should probably simplify it at some stage, but we haven't found time for that yet.


I haven't tried this, but my idea is to use FlutterWebviewPlugin to send the user to a URL like https://www.facebook.com/v2.8/dialog/oauth?client_id={app-id}&redirect_uri=fbAPP_ID://authorize. Then add native handlers for application:openURL:options: (on iOS) and onNewIntent (Android) and modify AndroidManifest.xml and Info.plist to register the app to receive URLs from the fbAPP_ID scheme. You can use the platform channels to pass the deep link parameters back to Dart-land and call close() on the webview on the Dart side.