Oauth2 flow in Flutter app

7,834

Solution 1

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.

Solution 2

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.

Share:
7,834
Rick de Vries
Author by

Rick de Vries

Updated on December 03, 2022

Comments

  • Rick de Vries
    Rick de Vries over 1 year

    In the Flutter app I'm currently building, I need to authenticate users against a custom (so non-Google/Facebook/Twitter/etc) authorization server.

    In order to achieve this, the users should fill in their credentials in a webpage. To this purpose, the WebView-plugin can be used. However, when the page is redirected after the user authenticated, the WebView should be closed, and the code passed on to the (Flutter) function that initially called the WebView.

    Having done some research already, I came accross the following options:

    • This blog post uses a local server, which still requires the user to manually close the window, which is not a real solution (in my opinion).
    • This issue marks integration with any OAuth provider as done, but does not provide any details on the user authentication inside the browser.
    • This issue is exactly like I am describing, but at the bottom it is mentioned that the WebView plugin provides a way to close the WebView. While it does indeed have a close()-function, I cannot find a way to trigger it on the redirect-URI and return the verification code.

    Does a solution exist, that closes the browser automatically once the redirect-URI is opened (and also returns the verification code)?

    Thanks in advance!

  • Rick de Vries
    Rick de Vries over 6 years
    This is what we ended up doing, but we used the URL-launcher to do it in the native browser instead of a WebView for security reasons. For example, the AndroidManifest.xml linked the URL to an Activity that just checked the URI data, sent it using the channel, and then close itself. Thanks!
  • straya
    straya about 6 years
    @RickdeVries what security reasons? AFAIK the external browser is only best-practice for "unofficial" clients (i.e. auth and client/app are not owned (code reviewed) by the same entity).
  • Rick de Vries
    Rick de Vries about 6 years
    @straya Our client is "semi-official": it is recommended by the owning entity, but everyone is free to request OAuth client IDs for it (none have yet). OAuth is all about trust, so we don't really see the reason to use the WebView instead (except maybe for the "stay-in-app experience"). The browser preserves the user's cookies, which is also a plus. Besides, our users are extremely tech-savvy, so some of them wouldn't even trust a WebView for their credentials, even if it was official.
  • straya
    straya about 6 years
    I'd trust my own code with a (modern)WebView a lot more than a browser which is having 3rd party code run within it constantly, has many more features than necessary for the auth use-case, and is subject to change (updates) a lot more. As for Users, a WebView for login should only be trusted for official software, so your decision makes sense.
  • Igor
    Igor about 6 years
    @RickdeVries, woudl you mind sharing your Activity code? I have modified the manifest to return the result to the activity based on the redirect URI but am stuck on what to do in the onActivityResult method of the Activity (and then how to build the channel to feed back into Flutter). My assumption is also that token request can be completed from the Flutter app based on the initially received authorization code.
  • Rick de Vries
    Rick de Vries about 6 years
    @Igor, I've posted a new answer containing the most important bits of our code