システム開発部のTです。
以前、iOS&Android開発でAuth0を組み込んだことがありましたが、
今回はFlutterでAuth0を組み込んでみようと思います。
記事が長くなるので、本編ではAndroidの実装のみにとどめます。
iOSについては、別記事にてiOS編として説明いたします。

Auth0とは

簡単に言うと、TwitterやFacebookなどが提供しているOAuth認証システムをひとまとめにしたような認証システムで、統合認証プラットフォームとも呼ばれています。
Auth0を利用することで、Twitter、Facebook、Google、Apple等のSNS認証を個別に実装する必要が無くなり、個別で実装するよりも工数が削減できるというメリットがあります。

詳しいことは、Auth0のサイトをご覧いただければと思います。

https://auth0.com/jp

前提準備

Auth0のアカウントはあらかじめ作成してください。
アカウント作成の手順は省略いたします。

Auth0管理画面でアプリ登録

まずはAuth0のアプリケーション一覧画面を表示し、画面右上の「+Create Application」ボタンを押下します。

次にNameに任意のアプリ名を設定し、Choose an application typeからNativeを選択後、「Create」ボタンを押下してください。

以下の画面が表示されます。
ここでSelect a native SDKと表示されていますが、各プラットフォームを押下すると簡単なセットアップ手順が記載された画面に遷移します。
本章でセットアップ手順を記載していくので、ここでは「Settings」を押下してください。

設定画面に切り替わるので、画面はこのままでAndroidStudioに切り替えてください。
また、Domain、ClientIDについては、AndroidStudioのプロジェクト設定時に使用します。

実装前事前準備

Android Studioを起動し、「New Flutter Project」を押下してください。

FlutterSDKのPathを入力後、「Next」ボタンを押下してください。

Project nameに任意のアプリ名を設定し、「Finish」を押下してください。
Organizationについても任意のドメインを設定してください。
本件では特にリリース予定もないため、初期値としています。

上記後、以下の画面が出ることを確認してください。

プロジェクトのツリーを展開し、ツリー上の「Android」を選択後、右クリックメニューから「Flutter」→「Open Android module in Android Studio」を押下すると、Flutterプロジェクト内部のAndroidプロジェクトを開くことができます。この手順で開かなくても直接設定を変更することは可能ですが、コード補完ができないので、個人的にはこの手順で開くことを勧めています。

サブプロジェクトのAndroidプロジェクトが開きます。

以降で説明する手順については、QuickStartに記載されているセットアップ手順を元に記載しております。以降の手順にて動作しない場合、セットアップ手順が更新されている場合もあるので、その場合、以下の画面から参照ください。

appのbuild.gradleを開き、dependenciesにAuth0のライブラリを追加します。

  implementation 'com.auth0.android:auth0:2.+'

同じく、defaultConfigに以下の内容を追加し、「Sync Now」を押下してください。

manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "demo"]

AndroidManifest.xmlを開き、以下の内容を追記してください。

<uses-permission android:name="android.permission.INTERNET" />

Auth0のアプリ設定画面に戻って、Allowed Callback URLsとAllowed Logout URLsに以下の内容を設定してください。
(※「○○○○」にはAuth0のユーザーIDを設定してください)

demo://OOOO.auth0.com/android/com.example.flutterauthapp/callback

その後、画面下までスクロールして、「Save Changes」を押下してください。

事前準備は以上になります。
ここから実装になります。

MainActivityの実装

Androidプロジェクト上のMainActivityに実装します。
まず、MainActivityの中身を覗いてみましょう。

class MainActivity: FlutterActivity() {
}

ごらんのとおり、基本的にはFlutter上で処理を行っている関係上、本来Androidネイティブ側の実装は必要ないため、上記のようになっています。

今回、FlutterとAndroid(およびiOS)側との連携が必要なため、上記画面にMethodChannelを通じてインターフェースを構築していきたいと思います。MethodChannelとは異なるプラットフォームを接続するためのFlutterAPIになります。

以下の内容にて実装していきましょう。

class MainActivity: FlutterActivity(), MethodCallHandler { // <-- Flutter側からメッセージを受け取るためのハンドラを設定
    companion object {
        // Method Channelのチャンネル名になります。
        // この名前で各プラットフォーム間の連携を行うことになります。
        private const val AUTH0_CHANNEL = "com.example.flutterauthapp/auth0"
    }

    private lateinit var channel: MethodChannel

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // MethodChannelにてチャンネル名を登録
        channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AUTH0_CHANNEL)
        // methodCannelに対して以下のonMethodCallを登録、Flutter側からの呼び出しに応答する
        channel.setMethodCallHandler(this)
    }

    // Flutter側からコールされたときの処理を実装
    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "isLogin" -> {
                // ログイン有無の状態取得処理を実装
            }
            "login" -> {
                // ログイン処理を実装
            }
            "getUserProfile" -> {
                // ユーザー情報取得処理を実装
            }
            "logout" -> {
                // ログアウト処理を実装
            }
            else -> result.notImplemented()
        }
    }
}

onMethodCall内部の実装については、以降で説明します。

本件では、MethodChannelについての説明については省略しますが、AUTH0_CHANNELの文字列については、大体「アプリパッケージ名/任意の文字列」とすることが多いようなので、本編でもそれに習って「com.example.flutterauthapp/auth0」と定義しました。

AUTH0_CHANNELの文字列については、Flutter側からAndroid側のAPI連携時に利用するので、そのときは、ここで定義した文字列と同じ文字列にするようにしてください。

ここまでの実装で、Flutter側からの呼び出しには対応しましたので、
以降では、Auth0の実装に進みます。

Auth0の定義追加

リソースファイル上にAuth0の定義を登録します。
strings.xmlを開いて、以下の内容を設定してください。
strings.xmlが存在しない場合は、作成してください。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="com_auth0_client_id">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</string>
    <string name="com_auth0_domain">OOOOOO.auth0.com</string>
    <string name="com_auth0_scheme">demo</string>
</resources>

上記のXXXX…XXXX には以下の画面のClientIDを設定してください。
OOOOOOにはユーザーIDを設定してください。以下の画面のDomainと同様の内容を設定します。
schemeには任意の内容になりますが、本件では「demo」と設定してください。

Auth0Utilsの実装

Auth0の処理を以下の内容で実装します。

import android.app.Activity
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException
import com.auth0.android.authentication.storage.SecureCredentialsManager
import com.auth0.android.authentication.storage.SharedPreferencesStorage
import com.auth0.android.callback.Callback
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.result.Credentials
import com.auth0.android.result.UserProfile
import io.flutter.plugin.common.MethodChannel

class Auth0Utils(private val activity: Activity) {
    private val auth0: Auth0 =
            Auth0(
                    activity.getString(R.string.com_auth0_client_id),
                    activity.getString(R.string.com_auth0_domain)
            )
    private val secureCredentialsManager =
            SecureCredentialsManager(
                    activity.applicationContext,
                    AuthenticationAPIClient(auth0),
                    SharedPreferencesStorage(activity.applicationContext)
            )

    fun isLogin(result: MethodChannel.Result) {
        result.success(secureCredentialsManager.hasValidCredentials())
    }

    fun login(result: MethodChannel.Result) {
        WebAuthProvider.login(auth0)
                .withScheme(activity.getString(R.string.com_auth0_scheme))
                .withScope("openid profile email")
                .start(activity, object : Callback<Credentials, AuthenticationException> {
                    override fun onFailure(error: AuthenticationException) {
                        result.error(error.getCode(), error.getDescription(), error)
                        return
                    }

                    override fun onSuccess(credentials: Credentials) {
                        try {
                            secureCredentialsManager.saveCredentials(credentials)
                            val credentialsMap = HashMap<String, Any?>()
                            credentialsMap["idToken"] = credentials.idToken
                            credentialsMap["accessToken"] = credentials.accessToken
                            credentialsMap["expiresAt"] = credentials.expiresAt.time
                            credentialsMap["refreshToken"] = credentials.refreshToken
                            credentialsMap["type"] = credentials.type
                            credentialsMap["scope"] = credentials.scope
                            credentialsMap["recoveryCode"] = credentials.recoveryCode
                            result.success(credentialsMap)
                        } catch (e:Exception) {
                            secureCredentialsManager.clearCredentials()
                            result.error(e.javaClass.simpleName, e.message, e)
                        }
                    }
                })
    }

    fun getUserProfile(result: MethodChannel.Result) {
        secureCredentialsManager.getCredentials(object : Callback<Credentials, CredentialsManagerException> {
            override fun onFailure(error: CredentialsManagerException) {
                result.error("CredentialsManagerException", error.message, error)
                return
            }

            override fun onSuccess(credentials: Credentials) {
                try {
                    val accessToken = credentials.accessToken
                    val authenticationClient = AuthenticationAPIClient(auth0)
                    authenticationClient.userInfo(accessToken)
                            .start(object : Callback<UserProfile, AuthenticationException> {
                                override fun onSuccess(userInfo: UserProfile) {
                                    val userInfoMap = HashMap<String, Any?>()
                                    userInfoMap["id"] = userInfo.getId()
                                    userInfoMap["name"] = userInfo.name
                                    userInfoMap["nickname"] = userInfo.nickname
                                    userInfoMap["pictureURL"] = userInfo.pictureURL
                                    userInfoMap["email"] = userInfo.email
                                    userInfoMap["isEmailVerified"] = userInfo.isEmailVerified
                                    userInfoMap["familyName"] = userInfo.familyName
                                    userInfoMap["givenName"] = userInfo.givenName
                                    userInfoMap["createdAt"] = userInfo.createdAt?.time?:0

                                    result.success(userInfoMap)
                                }

                                override fun onFailure(error: AuthenticationException) {
                                    result.error(error.getCode(), error.getDescription(), error)
                                }
                            })
                } catch (e:Exception) {
                    secureCredentialsManager.clearCredentials()
                    result.error(e.javaClass.simpleName, e.message, e)
                }
            }
        })
    }

    fun logout(result: MethodChannel.Result) {
        WebAuthProvider.logout(auth0)
                .withScheme(activity.getString(R.string.com_auth0_scheme))
                .start(activity, object : Callback<Void?, AuthenticationException> {
                    override fun onFailure(error: AuthenticationException) {
                        result.error(error.getCode(), error.getDescription(), error)
                        return
                    }

                    override fun onSuccess(void: Void?) {
                        secureCredentialsManager.clearCredentials()
                        result.success(true)
                    }
                })
    }
}

上記より各ポイントとなる箇所を説明します。


    private val auth0: Auth0 =
            Auth0(
                    activity.getString(R.string.com_auth0_client_id),
                    activity.getString(R.string.com_auth0_domain)
            )

上記はAuth0のインスタンスを生成しています。
引数はClientIDとDomainです。


    private val secureCredentialsManager =
            SecureCredentialsManager(
                    activity.applicationContext,
                    AuthenticationAPIClient(auth0),
                    SharedPreferencesStorage(activity.applicationContext)
            )

ログイン後のログイン情報をセキュアに格納するためのクラスです。
ログイン中は永続的に管理し、APIを利用するときに使うアクセストークンも
このManagerから取得することになります。


    fun isLogin(result: MethodChannel.Result) {
        result.success(secureCredentialsManager.hasValidCredentials())
    }

ログイン有無をsecureCredentialsManagerから取得し、それをFlutter側に通知しています。
メソッドの引数のMethodChannel.resultを利用して、Flutter側のコールバックに通知することができます。result.successには任意の値を設定可能ですが、以下に記載されている型以外はサポートされていません。

https://api.flutter.dev/javadoc/io/flutter/plugin/common/StandardMessageCodec.html

本メソッドでは、Boolean値を返しています。


    fun login(result: MethodChannel.Result) {
        WebAuthProvider.login(auth0)
                .withScheme(activity.getString(R.string.com_auth0_scheme))
                .withScope("openid profile email")
                .start(activity, object : Callback<Credentials, AuthenticationException> {
                    override fun onFailure(error: AuthenticationException) {
                        result.error(error.getCode(), error.getDescription(), error)
                        return
                    }

                    override fun onSuccess(credentials: Credentials) {
                        try {
                            secureCredentialsManager.saveCredentials(credentials)
                            val credentialsMap = HashMap<String, Any?>()
                            credentialsMap["idToken"] = credentials.idToken
                            credentialsMap["accessToken"] = credentials.accessToken
                            credentialsMap["expiresAt"] = credentials.expiresAt.time
                            credentialsMap["refreshToken"] = credentials.refreshToken
                            credentialsMap["type"] = credentials.type
                            credentialsMap["scope"] = credentials.scope
                            credentialsMap["recoveryCode"] = credentials.recoveryCode
                            result.success(credentialsMap)
                        } catch (e:Exception) {
                            secureCredentialsManager.clearCredentials()
                            result.error(e.javaClass.simpleName, e.message, e)
                        }
                    }
                })
    }

実際のログイン処理になります。
上記が実行されたときに、外部ブラウザが開きAuth0のログイン画面に遷移します。
ログイン画面にてログイン後、その結果がstartに定義されたコールバック上に戻ることで、
ログイン情報を取得することができます。

本件では、取得したログイン情報をsecureCredentialsManager上に保持し、
ログイン情報をFlutter側に通知する処理を実装しています。
通知の際、ログイン情報が一項目ではないため、Map定義で渡しています。

ログイン処理でエラーだった場合、result.error()を利用してFlutter側にエラー通知をしています。


    fun getUserProfile(result: MethodChannel.Result) {
        secureCredentialsManager.getCredentials(object : Callback<Credentials, CredentialsManagerException> {
            override fun onFailure(error: CredentialsManagerException) {
                result.error("CredentialsManagerException", error.message, error)
                return
            }

            override fun onSuccess(credentials: Credentials) {
                try {
                    val accessToken = credentials.accessToken
                    val authenticationClient = AuthenticationAPIClient(auth0)
                    authenticationClient.userInfo(accessToken)
                            .start(object : Callback<UserProfile, AuthenticationException> {
                                override fun onSuccess(userInfo: UserProfile) {
                                    val userInfoMap = HashMap<String, Any?>()
                                    userInfoMap["id"] = userInfo.getId()
                                    userInfoMap["name"] = userInfo.name
                                    userInfoMap["nickname"] = userInfo.nickname
                                    userInfoMap["pictureURL"] = userInfo.pictureURL
                                    userInfoMap["email"] = userInfo.email
                                    userInfoMap["isEmailVerified"] = userInfo.isEmailVerified
                                    userInfoMap["familyName"] = userInfo.familyName
                                    userInfoMap["givenName"] = userInfo.givenName
                                    userInfoMap["createdAt"] = userInfo.createdAt?.time?:0

                                    result.success(userInfoMap)
                                }

                                override fun onFailure(error: AuthenticationException) {
                                    result.error(error.getCode(), error.getDescription(), error)
                                }
                            })
                } catch (e:Exception) {
                    secureCredentialsManager.clearCredentials()
                    result.error(e.javaClass.simpleName, e.message, e)
                }
            }
        })
    }

secureCredentialsManagerからアクセストークンを取得し、Auth0のUserInfoAPIにアクセストークンを渡して実行すると、UserProfile情報が取得できます。
UserProfile情報をMapに格納し、resultでFlutter側に通知しています。
ユーザー情報取得エラー、またはログイン情報取得エラーだった場合、Flutter側にはエラーで返却しています。


    fun logout(result: MethodChannel.Result) {
        WebAuthProvider.logout(auth0)
                .withScheme(activity.getString(R.string.com_auth0_scheme))
                .start(activity, object : Callback<Void?, AuthenticationException> {
                    override fun onFailure(error: AuthenticationException) {
                        result.error(error.getCode(), error.getDescription(), error)
                        return
                    }

                    override fun onSuccess(void: Void?) {
                        secureCredentialsManager.clearCredentials()
                        result.success(true)
                    }
                })
    }

最後にログアウト処理です。
上記を実行することで、一瞬だけ外部ブラウザが起動し、すぐにアプリ側に戻ってきます。
正常時、secureCredentialsManager.clearCredentials()を実行しログイン情報破棄しています。
結果は正常終了を意味するTrueを返却しています。

エラーだった場合、falseではなくエラー内容を含めたエラーレスポンスを返却します。

以上で、Auth0Utilsの実装終了です。
以下、MainActivityに戻ります。

MainActivityに実装追加

以下の内容で実装追加します。

class MainActivity: FlutterActivity(), MethodCallHandler {
    companion object {
        // Method Channelのチャンネル名になります。
        // この名前で各プラットフォーム間の連携を行うことになります。
        private const val AUTH0_CHANNEL = "com.example.flutterauthapp/auth0"
    }

    private lateinit var channel: MethodChannel
    private lateinit var auth0Utils: Auth0Utils // <--- フィールドに追加

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        auth0Utils = Auth0Utils(this) // <--- ここでインスタンス生成

        // MethodChannelにてチャンネル名を登録
        channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AUTH0_CHANNEL)
        // methodCannelに対して以下のonMethodCallを登録、Flutter側からの呼び出しに応答する
        channel.setMethodCallHandler(this)
    }

    // Flutter側からコールされたときの処理を実装
    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "isLogin" -> {
                // ログイン有無の状態取得処理を実装
                auth0Utils.isLogin(result) // <--- ここに追加
            }
            "login" -> {
                // ログイン処理を実装
                auth0Utils.login(result) // <--- ここに追加
            }
            "getUserProfile" -> {
                // ユーザー情報取得処理を実装
                auth0Utils.getUserProfile(result) // <--- ここに追加
            }
            "logout" -> {
                // ログアウト処理を実装
                auth0Utils.logout(result) // <--- ここに追加
            }
            else -> result.notImplemented()
        }
    }
}

以上で、Android側のプロジェクトの実装は終了です。
以降は、Flutter側に戻っての実装になります。
おつかれかと思いますが、もう少しだけお付き合いいただければと思います。

Flutter側の実装

(※iOSで、すでに実装済みの場合、動作確認まで進んでください)

MVVMパターンでの実装をしていくので、あらかじめ以下のライブラリをdependeciesに定義してください。
MVVMパターンについては、以下の記事を参照ください。

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^1.0.0-dev.7  # <-- これを追加

Entityクラスの実装

Android側からログイン時、およびユーザー情報取得時に通知される情報を受け取るためのEntityクラスを実装します。

まずは、ログイン情報です。

class Credentials {
  late String idToken;
  late String accessToken;
  late String type;
  String? refreshToken;
  late int expiresAt;
  String? scope;
  String? recoveryCode;

  Credentials.createFromMap(Map<String, dynamic> data) {
    idToken = data["idToken"];
    accessToken = data["accessToken"];
    type = data["type"];
    refreshToken = data["refreshToken"];
    expiresAt = data["expiresAt"];
    scope = data["scope"];
    recoveryCode = data["recoveryCode"];
  }
}

次にユーザー情報。

class UserProfile {
  String? id;
  String? name;
  String? nickname;
  String? pictureURL;
  String? email;
  bool? isEmailVerified;
  String? familyName;
  String? givenName;
  int? createdAt;

  UserProfile.createFromMap(Map<String, dynamic> data) {
    id = data['id'];
    name = data['name'];
    nickname = data['nickname'];
    pictureURL = data['pictureURL'];
    email = data['email'];
    isEmailVerified = data['isEmailVerified'];
    familyName = data['familyName'];
    givenName = data['givenName'];
    createdAt = data['createdAt'];
  }
}

ViewModelクラスの実装

本クラスはAndroid側の処理を呼び出す処理です。
まずは実装全体になります。

import 'package:flutterauthapp/entity/user_profile.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final myHomeViewModelProvider = ChangeNotifierProvider((ref) => MyHomeViewModel());

class MyHomeViewModel extends ChangeNotifier {

  // MethodChannel名はAndroid側と同一にしておくこと
  static const MethodChannel _channel = MethodChannel('com.example.flutterauthapp/auth0');

  String loginButtonlabel = "Login";

  String name = "";
  String email = "";
  String imageUrl = "";

  Future<bool> isLogin() async {
    return await _channel.invokeMethod('isLogin');
  }

  Future<bool> login() async {
    try {
      Map<String, dynamic>? loginData = await _channel.invokeMapMethod<String, dynamic>('login');
      if (loginData != null) {
        loginButtonlabel = "Logout";
        notifyListeners();
        return true;
      }
      return false;
    } catch(e) {
      debugPrint(e.toString());
      return false;
    }
  }

  Future<bool> getUserProfile() async {
    try {
      Map<String, dynamic>? userProfile = await _channel.invokeMapMethod<String, dynamic>('getUserProfile');
      if (userProfile != null) {
        var user = UserProfile.createFromMap(userProfile);
        name = user.name ?? "";
        email = user.email ?? "";
        imageUrl = user.pictureURL ?? "";
        notifyListeners();
        return true;
      }
      return false;
    } catch(e) {
      debugPrint(e.toString());
      return false;
    }
  }

  Future<void> logout() async {
    try {
      bool isSuccess = await _channel.invokeMethod('logout');
      if (isSuccess) {
        loginButtonlabel = "Login";
        name = "";
        email = "";
        imageUrl = "";
        notifyListeners();
      }
    } catch(e) {
      debugPrint(e.toString());
    }
  }

}

以降、コードの説明です。

final myHomeViewModelProvider = ChangeNotifierProvider((ref) => MyHomeViewModel());

ProviderをDIコンテナ扱いとし、本クラスのViewModelのインスタンスを格納しておきます。


  static const MethodChannel _channel = MethodChannel('com.example.flutterauthapp/auth0');

Android側の実装と同一の名前のMethodChannelにすることで、Platform間での呼び出しに対応可能になります。


  Future<bool> isLogin() async {
    return await _channel.invokeMethod('isLogin');
  }

_channel.invokeMethod('isLogin')で各Platform上のisLogin()にアクセスできます。
ただし、awaitをつけていることでも分かるとおり、MethodChannelを利用する場合は全て非同期アクセスになります。

上記の処理では、ログイン有無を取得し呼び出し元にboolを返却しています。


  Future<bool> login() async {
    try {
      Map<String, dynamic>? loginData = await _channel.invokeMapMethod<String, dynamic>('login');
      if (loginData != null) {
        loginButtonlabel = "Logout";
        notifyListeners();
        return true;
      }
      return false;
    } catch(e) {
      debugPrint(e.toString());
      return false;
    }
  }

Android側のログイン処理をコールしています。
Android側の実装見てもらうとわかりますが、Mapで返しているので、本件ではinvokeMapMethodでログイン処理をコールしています。ログインデータのnullチェック後に、正常であればボタンのラベルを「Logout」に変更しています。一応nullチェックを実装していますが、nullが返却される状況の場合、Android側では例外を返却しているので、結果的にcatchに進みます。


  Future<bool> getUserProfile() async {
    try {
      Map<String, dynamic>? userProfile = await _channel.invokeMapMethod<String, dynamic>('getUserProfile');
      if (userProfile != null) {
        var user = UserProfile.createFromMap(userProfile);
        name = user.name ?? "";
        email = user.email ?? "";
        imageUrl = user.pictureURL ?? "";
        notifyListeners();
        return true;
      }
      return false;
    } catch(e) {
      debugPrint(e.toString());
      return false;
    }
  }

Auth0のUserInfoAPIにアクセスし、ユーザー情報を取得する処理です。
Map形式で返却されるため、nullチェック後にUserProfile.createFromMap(userProfile)でインスタンス生成&パース処理しています。取得したユーザー情報のうち、名前、E-mail、イメージ画像を取得し、画面上に反映しています。


  Future<void> logout() async {
    try {
      bool isSuccess = await _channel.invokeMethod('logout');
      if (isSuccess) {
        loginButtonlabel = "Login";
        name = "";
        email = "";
        imageUrl = "";
        notifyListeners();
      }
    } catch(e) {
      debugPrint(e.toString());
    }
  }

Auth0のログアウト処理になります。
上記を実行することで、一瞬だけ外部ブラウザが起動し、外部ブラウザ側でログアウト処理後、本アプリに戻ってログアウト正常終了時は、ボタンラベルを「Login」に変更、名前、E-mail、画像を初期化しています。

ViewModel側の実装は以上になります。

その他の実装

ここまで来たら、あと少しです。
残りの実装を見ていきましょう。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'my_app.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

特にありませんが、Providerを利用するため、ProviderScopeで起動アプリクラスをラッピングしています。


import 'package:flutter/material.dart';
import 'my_home_page.dart';

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

こちらも特になし。
プロジェクト生成時のMyAppクラスの内容そのものです。


import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'my_home_page_viewmodel.dart';

class MyHomePage extends ConsumerWidget {

  final String title;

  MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final _viewModel = ref.watch(myHomeViewModelProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Auth0 Test App',
            ),
            SizedBox(
              child: ElevatedButton(
                onPressed: () async {
                  var isLogin = await _viewModel.isLogin();
                  if (isLogin) {
                    await _viewModel.logout();
                  } else {
                    bool isSuccess = await _viewModel.login();
                    if (isSuccess) {
                      await _viewModel.getUserProfile();
                    }
                  }
                },
                child: Text(_viewModel.loginButtonlabel),
              ),
            ),
            _viewModel.imageUrl.isNotEmpty?Image.network(_viewModel.imageUrl):Container(),
            Text(
              'name: ${_viewModel.name}',
            ),
            Text(
              'email: ${_viewModel.email}',
            ),
          ],
        ),
      ),
    );
  }
}

実際の画面レイアウトを実装していますが、ConsumerWidgetを継承したクラスとなっております。

ボタン押下時、ViewModelからログイン有無を確認し、ログイン済みであればログアウト処理を、未ログインであればログイン処理をコールする処理をしています。ログイン成功時は、そのままユーザー情報取得処理をコールしています。

以上、全ての実装になります。

動作確認

プライベート的なところもあり動画で見せられないため、静止画でお見せいたします。

ログイン前
ログインボタンを押下
Auth0ログイン画面
Google認証を利用
ログイン後

いかがでしょうか。
うまく上記のイメージどおりに動いたでしょうか。
もし、上記のログイン画面が表示されなかったり、ログイン後の画面が表示されなかったりする場合は、Auth0側のコールバックURLを疑ってみてください。また、MethodChannelの名前が呼び出し元、先で同一であるか?も見直してみましょう。

まとめ

いかがだったでしょうか。
Flutterでの実装というよりは、ガッツリとAndroid側の実装でしたね・・・。
2021年9月28日の現状、Auth0はFlutter用のライブラリを用意していないため、MethodChannelを利用しての実装となりました。もし、今後Flutter用のライブラリが用意されたのなら、今以上に楽な実装になるかもしれません。

今回はそういったなかで、MethodChannelを利用する機会があったので、非常に勉強になりました。
本件ではAndroidでの実装でしたが、iOS版の記事もありますので、合わせて読んでいただければと思います。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる