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

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」を押下してください。

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

実装前事前準備

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

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

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

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

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

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

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

まずは、以下のようにRunner下にAutho.plistファイルを追加します。

Auth0.plistをSource Codeで開いてください。
その後、以下の内容を設定してください。
(※ClientID、Domainについては、Auth0の設定画面に記載されている内容を設定)

<!-- Auth0.plist -->

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>ClientId</key>
  <string>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</string>
  <key>Domain</key>
  <string>OOOOOO.auth0.com</string>
</dict>
</plist>

続けて、info.plistをSource Codeで開いてください。

以下の内容を追記します。

<!-- Info.plist -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>None</string>
        <key>CFBundleURLName</key>
        <string>auth0</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>

以下のように追記します。

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

com.example.flutterauthapp://OOOO.auth0.com/ios/com.example.flutterauthapp/callback

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

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

AppDelegateの実装

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

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

基本的にはFlutter関連の最低限の実装のみの状態です。

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

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

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)

        // Flutter側との連携のため、以下の3行を追加
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let methodChannel = FlutterMethodChannel(name: "com.example.flutterauthapp/auth0", binaryMessenger: controller as! FlutterBinaryMessenger)
        methodChannel.setMethodCallHandler(self.handle)

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

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

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

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

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

Auth0ライブラリの反映

今回はSwift Package Managerにて反映します。
「File」→「Swift Packages」→「Add Package Dependency…」を押下してください。

Choose Package Repositoryの検索入力部に以下のURLを設定し、「Next」を押下します。

https://github.com/auth0/Auth0.swift.git

以下の画面が表示されるので、そのまま「Next」を押下します。

読み込み中・・・

しばらくすると、以下の画面になるので、「Finish」で反映完了です。

Auth0Utilsの実装

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

import Foundation
import Auth0

class Auth0Utils {
    private static let credentialsManager = CredentialsManager(authentication: Auth0.authentication())

    static func isLogin(result: @escaping FlutterResult) {
        result(credentialsManager.hasValid())
    }

    static func login(result: @escaping FlutterResult) {
        let filePath = Bundle.main.path(forResource: "Auth0", ofType:"plist" )
        let plist = NSDictionary(contentsOfFile: filePath!)!
        let domain = plist["Domain"] as? String

        guard let domain = domain else { return }
        Auth0
            .webAuth()
            .scope("openid profile email")
            .audience("https://\(domain)/userinfo")
            .start { audienceResult in
                switch audienceResult {
                case .failure(let error):
                    result(FlutterError(code: "error", message: error.localizedDescription, details: nil))
                case .success(let credentials):
                    _ = credentialsManager.store(credentials: credentials)
                    var credentialsMap = [String: Any]()
                    credentialsMap["idToken"] = credentials.idToken
                    credentialsMap["accessToken"] = credentials.accessToken
                    credentialsMap["expiresAt"] = Int(credentials.expiresIn?.timeIntervalSince1970 ?? 0)
                    credentialsMap["refreshToken"] = credentials.refreshToken
                    credentialsMap["type"] = credentials.tokenType
                    credentialsMap["scope"] = credentials.scope
                    credentialsMap["recoveryCode"] = credentials.recoveryCode
                    result(credentialsMap)
                }
        }
    }
    
    static func logout(result: @escaping FlutterResult) {
        Auth0
            .webAuth()
            .clearSession(federated:false){
                switch $0{
                    case true:
                        _ = credentialsManager.clear()
                        result(true)
                    case false:
                        result(FlutterError(code: "error", message: "Logout error", details: nil))
                    }
                }
    }
    
    static func getUserProfile(result: @escaping FlutterResult) {
        credentialsManager.credentials { error, credentials in
            if let accessToken = credentials?.accessToken {
                Auth0
                    .authentication()
                    .userInfo(withAccessToken: accessToken)
                    .start { userInfoResult in
                        switch userInfoResult {
                        case .failure(let error):
                            result(FlutterError(code: "error", message: error.localizedDescription, details: nil))
                        case .success(let userInfo):
                            var userInfoMap = [String: Any]()
                            userInfoMap["id"] = userInfo.sub
                            userInfoMap["name"] = userInfo.name
                            userInfoMap["nickname"] = userInfo.nickname
                            userInfoMap["pictureURL"] = userInfo.picture?.absoluteString ?? ""
                            userInfoMap["email"] = userInfo.email
                            userInfoMap["isEmailVerified"] = userInfo.emailVerified
                            userInfoMap["familyName"] = userInfo.familyName
                            userInfoMap["givenName"] = userInfo.givenName
                            userInfoMap["createdAt"] = Int(userInfo.updatedAt?.timeIntervalSince1970 ?? 0)

                            result(userInfoMap)
                        }
                    }
            } else {
                result(FlutterError(code: "error", message: error?.localizedDescription ?? "", details: nil))
            }
        }
    }
}

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


private static let credentialsManager = CredentialsManager(authentication: Auth0.authentication())

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


static func isLogin(result: @escaping FlutterResult) {
	result(credentialsManager.hasValid())
}

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

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

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


static func login(result: @escaping FlutterResult) {
	let filePath = Bundle.main.path(forResource: "Auth0", ofType:"plist" )
	let plist = NSDictionary(contentsOfFile: filePath!)!
	let domain = plist["Domain"] as? String

	guard let domain = domain else { return }
	Auth0
		.webAuth()
		.scope("openid profile email")
		.audience("https://\(domain)/userinfo")
		.start { audienceResult in
			switch audienceResult {
			case .failure(let error):
				result(FlutterError(code: "error", message: error.localizedDescription, details: nil))
			case .success(let credentials):
				_ = credentialsManager.store(credentials: credentials)
				var credentialsMap = [String: Any]()
				credentialsMap["idToken"] = credentials.idToken
				credentialsMap["accessToken"] = credentials.accessToken
				credentialsMap["expiresAt"] = Int(credentials.expiresIn?.timeIntervalSince1970 ?? 0)
				credentialsMap["refreshToken"] = credentials.refreshToken
				credentialsMap["type"] = credentials.tokenType
				credentialsMap["scope"] = credentials.scope
				credentialsMap["recoveryCode"] = credentials.recoveryCode
				result(credentialsMap)
			}
	}
}

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

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

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


static func getUserProfile(result: @escaping FlutterResult) {
	credentialsManager.credentials { error, credentials in
		if let accessToken = credentials?.accessToken {
			Auth0
				.authentication()
				.userInfo(withAccessToken: accessToken)
				.start { userInfoResult in
					switch userInfoResult {
					case .failure(let error):
						result(FlutterError(
                                                              code: "error", 
                                                              message: error.localizedDescription, 
                                                              details: nil))
					case .success(let userInfo):
						var userInfoMap = [String: Any]()
						userInfoMap["id"] = userInfo.sub
						userInfoMap["name"] = userInfo.name
						userInfoMap["nickname"] = userInfo.nickname
						userInfoMap["pictureURL"] = userInfo.picture?.absoluteString ?? ""
						userInfoMap["email"] = userInfo.email
						userInfoMap["isEmailVerified"] = userInfo.emailVerified
						userInfoMap["familyName"] = userInfo.familyName
						userInfoMap["givenName"] = userInfo.givenName
						userInfoMap["createdAt"] = 
                                                         Int(userInfo.updatedAt?.timeIntervalSince1970 ?? 0)

						result(userInfoMap)
					}
				}
		} else {
			result(FlutterError(
                                  code: "error", 
                                  message: error?.localizedDescription ?? "", 
                                  details: nil))
		}
	}
}

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


static func logout(result: @escaping FlutterResult) {
	Auth0
		.webAuth()
		.clearSession(federated:false){
			switch $0{
				case true:
					_ = credentialsManager.clear()
					result(true)
				case false:
					result(FlutterError(
                                               code: "error", 
                                               message: "Logout error", 
                                               details: nil))
				}
			}
}

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

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

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

AppDelegateに実装追加

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

import UIKit
import Flutter
import Auth0

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)

        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let methodChannel = FlutterMethodChannel(name: "com.example.flutterauthapp/auth0", binaryMessenger: controller as! FlutterBinaryMessenger)

        // FlutterからのMethodCall時のハンドラを設定
        methodChannel.setMethodCallHandler(self.handle)

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // 以下のコールバックにて「Auth0.resumeAuth()」を実行
    override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
        return Auth0.resumeAuth(url, options: options)
    }

    // FlutterからのMethodCall時の処理を実装する
    private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
      switch call.method {
      case "isLogin":
        Auth0Utils.isLogin(result: result)
      case "login":
        Auth0Utils.login(result: result)
      case "getUserProfile":
        Auth0Utils.getUserProfile(result: result)
      case "logout":
        Auth0Utils.logout(result: result)
      default:
        result(FlutterMethodNotImplemented)
      }
    }

}

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

Flutter側の実装

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

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クラスの実装

iOS側からログイン時、およびユーザー情報取得時に通知される情報を受け取るための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クラスの実装

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

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名はiOS側と同一にしておくこと
  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');

iOS側の実装と同一の名前の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;
    }
  }

iOS側のログイン処理をコールしています。
iOS側の実装見てもらうとわかりますが、Mapで返しているので、本件ではinvokeMapMethodでログイン処理をコールしています。ログインデータのnullチェック後に、正常であればボタンのラベルを「Logout」に変更しています。一応nullチェックを実装していますが、nullが返却される状況の場合、iOS側では例外を返却しているので、結果的に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からログイン有無を確認し、ログイン済みであればログアウト処理を、ログイン未済であればログイン処理をコールする処理をしています。ログイン成功時は、そのままユーザー情報取得処理をコールしています。

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

動作確認

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

ログイン処理の動き

ログインボタン押下
ダイアログで「続ける」押下
ログイン実施
ログイン後

ログアウト時の動き

ログアウトボタン押下
ダイアログで「続ける」押下
ログアウト後

いかがでしょうか。
うまく上記のイメージどおりに動いたでしょうか。
ログイン、ログアウト時に、余計なダイアログが表示されますが、iOSだと表示されてしまいます。
Auth0側に問い合わせしましたが、現状iOSの仕様とのことです。

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

まとめ

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

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



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