MaikuB / flutter_appauth

A Flutter wrapper for AppAuth iOS and Android SDKs
273 stars 246 forks source link

App crashing when I try to use app_auth with flutter_downloader in a project #390

Closed NoshinDev closed 1 year ago

NoshinDev commented 1 year ago

I tried to add app_auth authorization to this basic project: https://github.com/fluttercommunity/flutter_downloader/blob/master/example/lib/main.dart

But after creating an authorization page and an authorization button redirecting to the web for data entry, the application crashes and loses connection to the project. If you skip the authorization and try to download the file there will be no problem, but if you erase all the implementations of flutter_downloader except the file pubscpec.yaml the result will be the crash of the application

My pubspec.yaml file looks as follows:

name: flutter_downloader_example
description: Demonstrates how to use the flutter_downloader plugin.
version: 1.0.0+1
publish_to: none

environment:
  sdk: ">=2.17.0 <3.0.0"
  flutter: ">=3.0.0 <4.0.0"

dependencies:
  android_path_provider: ^0.3.0
  device_info_plus: ^8.0.0
  flutter:
    sdk: flutter
  flutter_appauth: ^4.2.0
  flutter_downloader: ^1.9.1
  http: ^0.13.5
  path_provider: ^2.0.11
  permission_handler: ^10.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  leancode_lint: ^2.0.0+1

flutter:
  uses-material-design: true

Authorization file:

import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;

class Auth extends StatefulWidget {
  const Auth({super.key});

  @override
  State<Auth> createState() => _AuthState();
}

class _AuthState extends State<Auth> {
  bool _isBusy = false;
  final FlutterAppAuth _appAuth = const FlutterAppAuth();
  String? _codeVerifier;
  String? _nonce;
  String? _authorizationCode;
  String? _refreshToken;
  String? _accessToken;
  String? _idToken;

  final TextEditingController _authorizationCodeTextController =
      TextEditingController();
  final TextEditingController _accessTokenTextController =
      TextEditingController();
  final TextEditingController _accessTokenExpirationTextController =
      TextEditingController();

  final TextEditingController _idTokenTextController = TextEditingController();
  final TextEditingController _refreshTokenTextController =
      TextEditingController();
  String? _userInfo;

  // For a list of client IDs, go to https://demo.duendesoftware.com
  final String _clientId = 'interactive.public';
  final String _redirectUrl = 'com.duendesoftware.demo:/oauthredirect';
  final String _issuer = 'https://demo.duendesoftware.com';
  final String _discoveryUrl =
      'https://demo.duendesoftware.com/.well-known/openid-configuration';
  final String _postLogoutRedirectUrl = 'com.duendesoftware.demo:/';
  final List<String> _scopes = <String>[
    'openid',
    'profile',
    'email',
    'offline_access',
    'api'
  ];

  final AuthorizationServiceConfiguration _serviceConfiguration =
      const AuthorizationServiceConfiguration(
    authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize',
    tokenEndpoint: 'https://demo.duendesoftware.com/connect/token',
    endSessionEndpoint: 'https://demo.duendesoftware.com/connect/endsession',
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: SafeArea(
          child: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                Visibility(
                  visible: _isBusy,
                  child: const LinearProgressIndicator(),
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  child: const Text('Sign in with no code exchange'),
                  onPressed: () => _signInWithNoCodeExchange(),
                ),

                ElevatedButton(
                  child: const Text('Refresh token'),
                  onPressed: _refreshToken != null ? _refresh : null,
                ),
                const SizedBox(height: 8),
                ElevatedButton(
                  child: const Text('End session'),
                  onPressed: _idToken != null
                      ? () async {
                          await _endSession();
                        }
                      : null,
                ),
                const SizedBox(height: 8),
                const Text('authorization code'),
                TextField(
                  controller: _authorizationCodeTextController,
                ),
                const Text('access token'),
                TextField(
                  controller: _accessTokenTextController,
                ),
                const Text('access token expiration'),
                TextField(
                  controller: _accessTokenExpirationTextController,
                ),
                const Text('id token'),
                TextField(
                  controller: _idTokenTextController,
                ),
                const Text('refresh token'),
                TextField(
                  controller: _refreshTokenTextController,
                ),
                const Text('test api results'),
                Text(_userInfo ?? ''),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _endSession() async {
    try {
      _setBusyState();
      await _appAuth.endSession(EndSessionRequest(
          idTokenHint: _idToken,
          postLogoutRedirectUrl: _postLogoutRedirectUrl,
          serviceConfiguration: _serviceConfiguration));
      _clearSessionInfo();
    } catch (_) {}
    _clearBusyState();
  }

  void _clearSessionInfo() {
    setState(() {
      _codeVerifier = null;
      _nonce = null;
      _authorizationCode = null;
      _authorizationCodeTextController.clear();
      _accessToken = null;
      _accessTokenTextController.clear();
      _idToken = null;
      _idTokenTextController.clear();
      _refreshToken = null;
      _refreshTokenTextController.clear();
      _accessTokenExpirationTextController.clear();
      _userInfo = null;
    });
  }

  Future<void> _refresh() async {
    try {
      _setBusyState();
      final TokenResponse? result = await _appAuth.token(TokenRequest(
          _clientId, _redirectUrl,
          refreshToken: _refreshToken, issuer: _issuer, scopes: _scopes));
      _processTokenResponse(result);
      await _testApi(result);
    } catch (_) {
      _clearBusyState();
    }
  }

  Future<void> _exchangeCode() async {
    try {
      _setBusyState();
      final TokenResponse? result = await _appAuth.token(TokenRequest(
          _clientId, _redirectUrl,
          authorizationCode: _authorizationCode,
          discoveryUrl: _discoveryUrl,
          codeVerifier: _codeVerifier,
          nonce: _nonce,
          scopes: _scopes));
      _processTokenResponse(result);
      await _testApi(result);
    } catch (_) {
      _clearBusyState();
    }
  }

  Future<void> _signInWithNoCodeExchange() async {
    try {
      _setBusyState();
      // use the discovery endpoint to find the configuration
      final AuthorizationResponse? result = await _appAuth.authorize(
        AuthorizationRequest(_clientId, _redirectUrl,
            discoveryUrl: _discoveryUrl, scopes: _scopes, loginHint: 'bob'),
      );

      // or just use the issuer
      // var result = await _appAuth.authorize(
      //   AuthorizationRequest(
      //     _clientId,
      //     _redirectUrl,
      //     issuer: _issuer,
      //     scopes: _scopes,
      //   ),
      // );
      if (result != null) {
        _processAuthResponse(result);
      }
    } catch (_) {
      _clearBusyState();
    }
  }

  Future<void> _signInWithNoCodeExchangeAndGeneratedNonce() async {
    try {
      _setBusyState();
      final Random random = Random.secure();
      final String nonce =
          base64Url.encode(List<int>.generate(16, (_) => random.nextInt(256)));
      // use the discovery endpoint to find the configuration
      final AuthorizationResponse? result = await _appAuth.authorize(
        AuthorizationRequest(_clientId, _redirectUrl,
            discoveryUrl: _discoveryUrl,
            scopes: _scopes,
            loginHint: 'bob',
            nonce: nonce),
      );

      if (result != null) {
        _processAuthResponse(result);
      }
    } catch (_) {
      _clearBusyState();
    }
  }

  Future<void> _signInWithAutoCodeExchange(
      {bool preferEphemeralSession = false}) async {
    try {
      _setBusyState();

      // show that we can also explicitly specify the endpoints rather than getting from the details from the discovery document
      final AuthorizationTokenResponse? result =
          await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          _clientId,
          _redirectUrl,
          serviceConfiguration: _serviceConfiguration,
          scopes: _scopes,
          preferEphemeralSession: preferEphemeralSession,
        ),
      );

      // this code block demonstrates passing in values for the prompt parameter. in this case it prompts the user login even if they have already signed in. the list of supported values depends on the identity provider
      // final AuthorizationTokenResponse result = await _appAuth.authorizeAndExchangeCode(
      //   AuthorizationTokenRequest(_clientId, _redirectUrl,
      //       serviceConfiguration: _serviceConfiguration,
      //       scopes: _scopes,
      //       promptValues: ['login']),
      // );

      if (result != null) {
        _processAuthTokenResponse(result);
        await _testApi(result);
      }
    } catch (_) {
      _clearBusyState();
    }
  }

  void _clearBusyState() {
    setState(() {
      _isBusy = false;
    });
  }

  void _setBusyState() {
    setState(() {
      _isBusy = true;
    });
  }

  void _processAuthTokenResponse(AuthorizationTokenResponse response) {
    setState(() {
      _accessToken = _accessTokenTextController.text = response.accessToken!;
      _idToken = _idTokenTextController.text = response.idToken!;
      _refreshToken = _refreshTokenTextController.text = response.refreshToken!;
      _accessTokenExpirationTextController.text =
          response.accessTokenExpirationDateTime!.toIso8601String();
    });
  }

  void _processAuthResponse(AuthorizationResponse response) {
    setState(() {
      // save the code verifier and nonce as it must be used when exchanging the token
      _codeVerifier = response.codeVerifier;
      _nonce = response.nonce;
      _authorizationCode =
          _authorizationCodeTextController.text = response.authorizationCode!;
      _isBusy = false;
    });
  }

  void _processTokenResponse(TokenResponse? response) {
    setState(() {
      _accessToken = _accessTokenTextController.text = response!.accessToken!;
      _idToken = _idTokenTextController.text = response.idToken!;
      _refreshToken = _refreshTokenTextController.text = response.refreshToken!;
      _accessTokenExpirationTextController.text =
          response.accessTokenExpirationDateTime!.toIso8601String();
    });
  }

  Future<void> _testApi(TokenResponse? response) async {
    final http.Response httpResponse = await http.get(
        Uri.parse('https://demo.duendesoftware.com/api/test'),
        headers: <String, String>{'Authorization': 'Bearer $_accessToken'});
    setState(() {
      _userInfo = httpResponse.statusCode == 200 ? httpResponse.body : '';
      _isBusy = false;
    });
  }
}

image image image

MaikuB commented 1 year ago

The screenshot would seem to indicate you are using an Android emulator that hasn't been properly configured or an outdated device. This is based on how the screenshot shows an webview being opened instead of Chrome Custom tab and webviews aren't supported by the native AppAuth SDKs. You'll need to use an up to date device or use a properly configured emulator. From memory, the emulator needs to be one with Google Play services enabled

NoshinDev commented 1 year ago

The screenshot would seem to indicate you are using an Android emulator that hasn't been properly configured or an outdated device. This is based on how the screenshot shows an webview being opened instead of Chrome Custom tab and webviews aren't supported by the native AppAuth SDKs. You'll need to use an up to date device or use a properly configured emulator. From memory, the emulator needs to be one with Google Play services enabled

This also happens on real devices tested on android 12 E/AndroidRuntime(28156): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584) E/AndroidRuntime(28156): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034) E/AndroidRuntime(28156): Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity. E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:846) E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:809) E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatDelegateImpl.initWindowDecorActionBar(AppCompatDelegateImpl.java:550) E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatDelegateImpl.getSupportActionBar(AppCompatDelegateImpl.java:537) E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatDelegateImpl.invalidateOptionsMenu(AppCompatDelegateImpl.java:1220) E/AndroidRuntime(28156): at androidx.appcompat.app.AppCompatActivity.invalidateOptionsMenu(AppCompatActivity.java:314) E/AndroidRuntime(28156): at androidx.activity.ComponentActivity.invalidateMenu(ComponentActivity.java:553) E/AndroidRuntime(28156): at androidx.activity.ComponentActivity$$ExternalSyntheticLambda2.run(Unknown Source:2) E/AndroidRuntime(28156): at androidx.core.view.MenuHostHelper.addMenuProvider(MenuHostHelper.java:133) E/AndroidRuntime(28156): at androidx.activity.ComponentActivity.addMenuProvider(ComponentActivity.java:531) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentActivity$HostCallbacks.addMenuProvider(FragmentActivity.java:736) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentManager.attachController(FragmentManager.java:2784) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentController.attachHost(FragmentController.java:117) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentActivity.lambda$init$3$androidx-fragment-app-FragmentActivity(FragmentActivity.java:140) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentActivity$$ExternalSyntheticLambda0.onContextAvailable(Unknown Source:2) E/AndroidRuntime(28156): at androidx.activity.contextaware.ContextAwareHelper.dispatchOnContextAvailable(ContextAwareHelper.java:99) E/AndroidRuntime(28156): at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:352) E/AndroidRuntime(28156): at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:217) E/AndroidRuntime(28156): at net.openid.appauth.RedirectUriReceiverActivity.onCreate(RedirectUriReceiverActivity.java:49) E/AndroidRuntime(28156): at android.app.Activity.performCreate(Activity.java:8145) E/AndroidRuntime(28156): at android.app.Activity.performCreate(Activity.java:8125) E/AndroidRuntime(28156): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1344) E/AndroidRuntime(28156): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3750) E/AndroidRuntime(28156): ... 12 more I/Process (28156): Sending signal. PID: 28156 SIG: 9 Lost connection to device.

MaikuB commented 1 year ago

In that case this one looks like a different issue and the trace is same as what got raised in https://github.com/MaikuB/flutter_appauth/issues/362 so will close this as a duplicate. From what I know, what's happening is outside the control of this plugin. You may need to see what workarounds others have used in that issue