ThexXTURBOXx / flutter_web_auth_2

Flutter plugin for authenticating a user with a web service
https://pub.dev/packages/flutter_web_auth_2
MIT License
51 stars 50 forks source link

Android Login - popup with "Open with" #76

Closed timyyo closed 11 months ago

timyyo commented 11 months ago

Describe the bug

See screenshot, android shows after successful login a pup op to select an open with entry:

Expected behavior

If I select the first entry "flutter_web_auth_2" it works as expected. If I select the app name entry, i get this error in log:

ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: PlatformException(CANCELED, User canceled login, null, null) E/flutter (17624): #0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:652:7) E/flutter (17624): #1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:310:18)

Screenshots

Screenshot_20231008-105420

Device (please complete the following information!)

Additional context

Works all good on iOS. Using flutter_web_auth_2: ^2.2.1.

My AndroidMainfest.xml:

    <activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true">
        <intent-filter android:label="flutter_web_auth_2">
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="ch.company.appname"/>
        </intent-filter>
    </activity>

If I select within the open with the app name and say always the app does not work anymore and is always immediately redirected to the login screen.

Checklist

ThexXTURBOXx commented 11 months ago

It looks like you have added two activities which both can handle callbacks to your app (or some package has added another one). If that's the case, try to specify a better intent filter for the other activity. Thus, I would not consider this a bug. For further help, try to ask on StackOverflow.

timyyo commented 11 months ago

Thanks for your reply ThexXTURBOXx. Please see my full AndroidMainfest.xml, my other activity/intent-filter is the default that the app is even running?

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="ch.company.appname">
    <application
            android:label="appname"
            android:name="${applicationName}"
            android:icon="@mipmap/launcher_icon">
        <activity
                android:name=".MainActivity"
                android:exported="true"
                android:launchMode="singleTop"
                android:theme="@style/LaunchTheme"
                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:hardwareAccelerated="true"
                android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
                    android:name="io.flutter.embedding.android.NormalTheme"
                    android:resource="@style/NormalTheme"
            />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.linusu.flutter_web_auth_2.CallbackActivity" android:exported="true">
            <intent-filter android:label="flutter_web_auth_2">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="ch.company.appname"/>
                <data android:host="auth"/>
            </intent-filter>
        </activity>

             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
                android:name="flutterEmbedding"
                android:value="2"/>
    </application>
</manifest>

Within the login flow I also don't do anything fancy, just a regular flow:


final pkcePair = PkcePair.generate();

    final url = Uri(
        host: Config.AppnameAuthHost,
        scheme: Config.AppnameAuthScheme,
        path: '/authorize',
        queryParameters: {
          'code_challenge_method': 'S256',
          'scope': '',
          'response_type': 'code',
          'approval_prompt': 'auto',
          'redirect_uri': Config.authRedirectUri,
          'client_id': Config.AppnameAuthClientId,
          'code_challenge': pkcePair.codeChallenge,
          // 'force_logout_first': '1' // used to force a new login, no matter the fact that the user is already logged in on Appname-auth
        });

    final result = await FlutterWebAuth2.authenticate(
        url: url.toString(), callbackUrlScheme: Config.internalDeeplinkScheme);

    await SecureStorageService.storage.write(
      key: SecureStorageService.codeChallenge,
      value: pkcePair.codeChallenge,
    );

    final code = Uri.parse(result).queryParameters['code'];
    final Uri url2 = Uri(
        host: Config.AppnameAuthHost,
        path: '/token',
        scheme: Config.AppnameAuthScheme);
    final response = await http.post(url2, body: {
      'grant_type': 'authorization_code',
      'redirect_uri': Config.authRedirectUri,
      'code': code,
      'client_id': Config.AppnameAuthClientId,
      'code_verifier': pkcePair.codeVerifier,
    });

    final accessToken = jsonDecode(response.body)['access_token'] as String;
    return accessToken;
ThexXTURBOXx commented 11 months ago

Looks like you are using OAuth 2. If that is really the case, use a standardised solution such as oauth2_client which has built-in support for flutter_web_auth_2. Also, I cannot reproduce this behavior using the example app. If you can, I will gladly reopen this issue and fix it, but otherwise it seems like some other flutter package that your app uses adds another activity. You could use apktool to inspect the AndroidManifest.xml of a release of your app in order to see which activities your final app has.

adrian-moisa commented 4 months ago

I have the same issue and I think the answer is the same, I have added two activities. One was present, the new one is from the library. I will attempt to merge them. I think a mention in the readme.md about this issue could help. Cheers!

ThexXTURBOXx commented 4 months ago

I think a mention in the readme.md about this issue could help

Good point, will add when I have time!

adrian-moisa commented 4 months ago

I'll leave my findings for my own issue related to this ticket. Just in case somebody has a similar issue. Had the same behavior, browser was showing a popup "Would you like to leave firefox to view this content". After confirmation a new one appear asking me to select the app I want to use. I had my own app listed twice, just different icons. Took me a while to understand what is going on.

image

On top of the app I'm using patrol for e2e testing. Patrol has a dedicated main app which it uses to run the dart tests in android. In my infinite wisdom, I made another manifest for androidTest in which I put activity for flutter_web_auth_2, believing that patrol needs it for its login to work. I don't know how I managed to convince myself that the 2nd manifesto is needed. What actually happens:

The solution I deleted the manifest from androidTest, and we have flutter_web_auth_2 declared only once. Then the android sees that it has only one activity which it knows how to open the url with our scheme. As a result, it no longer panics to ask if you are ok to open the browser that link and choose which application. Android already knows what the plan is because there is only one. Now I no longer have the problem. Patrol tests work without problems. Now I no longer get my pipeline stuck.