luis901101 / oauth_webauth

BSD 3-Clause "New" or "Revised" License
15 stars 17 forks source link

OAuth WebAuth

This plugin offers a WebView implementation approach for OAuth authorization/authentication with identity providers. In the case of Web it doesn't use WebView, instead it loads the page directly in the browser.

Compatibility

Platform Compatibility
Android :white_check_mark:
iOS :white_check_mark:
Web :white_check_mark:

Preamble

Other plugins like Flutter AppAuth uses native implementation of AppAuth which in turn uses SFAuthenticationSession and CustomTabs for iOS and Android respectively. When using SFAuthenticationSession and CustomTabs your app will/could present some problems like:

Features

With this plugin you will get:

Notes:

Migration from ^1.0.0 to ^2.0.0

Migration from ^2.0.0 to ^3.0.0

Probably you will not need to do any migration. You only need to do changes:

Migration from ^3.0.0 to ^4.0.0

Migration from ^4.0.0 to ^5.0.0

Getting started

As stated before this plugin uses WebView implementation specifically the plugin flutter_inappwebview. For any WebView related problem please check the documentation of that plugin at docs.

Android setup

Just add the internet permission to your AndroidManifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.example">  
    <uses-permission android:name="android.permission.INTERNET"/>  
 <application>  
    ...
 </application>

iOS setup

Just add this to your Info.plist

<plist version="1.0">  
<dict>
    ....
    ....
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
    ....
    ....
</dict>  
</plist>

Web setup

To make it work properly on the Web platform, you need to add the web_support.js file inside the <head> of your web/index.html file:

<head>
    <!-- ... -->
    <script type="application/javascript" src="https://github.com/luis901101/oauth_webauth/raw/master/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
    <!-- ... -->
</head>

Usage

NOTE: all widgets can be extended to change/improve its features.

Initialization

In the main() function you should initialize this plugin just before runApp(...):

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await OAuthWebAuth.instance.init();
  runApp(const MyApp());
}

IMPORTANT:

OAuthWebView

An authorization/authentication process can get 3 outputs.

  1. User successfully authenticates, which returns an OAuth2 Credentials object with the access-token and refresh-token.
  2. An error occurred during authorization/authentication, or maybe certificate validation failed.
  3. User canceled authentication.

This plugin offers two variants to handle these outputs.

Variant 1

Awaiting response from navigator route Future.

void loginV1() async {
  final result = await OAuthWebScreen.start(
      context: context,
      configuration: OAuthConfiguration(
        authorizationEndpointUrl: authorizationEndpointUrl,
        tokenEndpointUrl: tokenEndpointUrl,
        clientSecret: clientSecret,
        clientId: clientId,
        redirectUrl: redirectUrl,
        scopes: scopes,
        promptValues: const ['login'],
        loginHint: 'johndoe@mail.com',
        onCertificateValidate: (certificate) {
          ///This is recommended
          /// Do certificate validations here
          /// If false is returned then a CertificateException() will be thrown
          return true;
        },
        textLocales: {
          ///Optionally texts can be localized
          BaseConfiguration.backButtonTooltipKey: 'Ir atrás',
          BaseConfiguration.forwardButtonTooltipKey: 'Ir adelante',
          BaseConfiguration.reloadButtonTooltipKey: 'Recargar',
          BaseConfiguration.clearCacheButtonTooltipKey: 'Limpiar caché',
          BaseConfiguration.closeButtonTooltipKey: 'Cerrar',
          BaseConfiguration.clearCacheWarningMessageKey:
          '¿Está seguro que desea limpiar la caché?',
        },
        contentLocale: Locale('es'),
        refreshBtnVisible: false,
        clearCacheBtnVisible: false,
      ));
  if (result != null) {
    if (result is Credentials) {
      authResponse = getPrettyCredentialsJson(result);
    } else {
      authResponse = result.toString();
    }
  } else {
    authResponse = 'User cancelled authentication';
  }
  setState(() {});
}

Variant2

Using callbacks

void loginV2() {
  OAuthWebScreen.start(
      context: context,
      configuration: OAuthConfiguration(
          authorizationEndpointUrl: authorizationEndpointUrl,
          tokenEndpointUrl: tokenEndpointUrl,
          clientSecret: clientSecret,
          clientId: clientId,
          redirectUrl: redirectUrl,
          scopes: scopes,
          promptValues: const ['login'],
          loginHint: 'johndoe@mail.com',
          onCertificateValidate: (certificate) {
            ///This is recommended
            /// Do certificate validations here
            /// If false is returned then a CertificateException() will be thrown
            return true;
          },
          textLocales: {
            ///Optionally text can be localized
            BaseConfiguration.backButtonTooltipKey: 'Ir atrás',
            BaseConfiguration.forwardButtonTooltipKey: 'Ir adelante',
            BaseConfiguration.reloadButtonTooltipKey: 'Recargar',
            BaseConfiguration.clearCacheButtonTooltipKey: 'Limpiar caché',
            BaseConfiguration.closeButtonTooltipKey: 'Cerrar',
            BaseConfiguration.clearCacheWarningMessageKey:
                '¿Está seguro que desea limpiar la caché?',
          },
          contentLocale: Locale('es'),
          refreshBtnVisible: false,
          clearCacheBtnVisible: false,
          onSuccessAuth: (credentials) {
            isLoading = false;
            setState(() {
              authResponse = getPrettyCredentialsJson(credentials);
            });
          },
          onError: (error) {
            isLoading = false;
            setState(() {
              authResponse = error.toString();
            });
          },
          onCancel: () {
            isLoading = false;
            setState(() {
              authResponse = 'User cancelled authentication';
            });
          }),
    );
}

BaseWebView

3 possible outputs.

  1. User is successfully redirected, which returns the full redirect url.
  2. An error occurred during navigation.
  3. User canceled web view.

This plugin offers two variants to handle these outputs.

Variant 1

Awaiting response from navigator route Future.

void baseRedirectV1() async {
  final result = await BaseWebScreen.start(
      context: context,
      configuration: BaseConfiguration(
        initialUrl: initialUrl,
        redirectUrls: [redirectUrl, baseUrl],
        onCertificateValidate: (certificate) {
          ///This is recommended
          /// Do certificate validations here
          /// If false is returned then a CertificateException() will be thrown
          return true;
        },
        textLocales: {
          ///Optionally texts can be localized
          BaseConfiguration.backButtonTooltipKey: 'Ir atrás',
          BaseConfiguration.forwardButtonTooltipKey: 'Ir adelante',
          BaseConfiguration.reloadButtonTooltipKey: 'Recargar',
          BaseConfiguration.clearCacheButtonTooltipKey: 'Limpiar caché',
          BaseConfiguration.closeButtonTooltipKey: 'Cerrar',
          BaseConfiguration.clearCacheWarningMessageKey:
              '¿Está seguro que desea limpiar la caché?',
        },
        contentLocale: Locale('es'),
        refreshBtnVisible: false,
        clearCacheBtnVisible: false,
      ),
    );
  if (result != null) {
    if (result is String) {
      /// If result is String it means redirected successful
      response = 'User redirected to: $result';
    } else {
      /// If result is not String then some error occurred
      response = result.toString();
    }
  } else {
    /// If no result means user cancelled
    response = 'User cancelled';
  }
  setState(() {});
}

Variant2

Using callbacks

void baseRedirectV2() {
  BaseWebScreen.start(
      context: context,
      configuration: BaseConfiguration(
          initialUrl: initialUrl,
          redirectUrls: [redirectUrl, baseUrl],
          onCertificateValidate: (certificate) {
            ///This is recommended
            /// Do certificate validations here
            /// If false is returned then a CertificateException() will be thrown
            return true;
          },
          textLocales: {
            ///Optionally text can be localized
            BaseConfiguration.backButtonTooltipKey: 'Ir atrás',
            BaseConfiguration.forwardButtonTooltipKey: 'Ir adelante',
            BaseConfiguration.reloadButtonTooltipKey: 'Recargar',
            BaseConfiguration.clearCacheButtonTooltipKey: 'Limpiar caché',
            BaseConfiguration.closeButtonTooltipKey: 'Cerrar',
            BaseConfiguration.clearCacheWarningMessageKey:
                '¿Está seguro que desea limpiar la caché?',
          },
          contentLocale: Locale('es'),
          refreshBtnVisible: false,
          clearCacheBtnVisible: false,
          onSuccessRedirect: (responseRedirect) {
            setState(() {
              response = 'User redirected to: $responseRedirect';
            });
          },
          onError: (error) {
            setState(() {
              response = error.toString();
            });
          },
          onCancel: () {
            setState(() {
              response = 'User cancelled';
            });
          }),
    );
}

Important notes

Authentication/Authorization services tested

Note: It should work with any OAuth2 comatible service that uses Authorization Code Grant