FlutterでAndroidでのAuth0のログイン・ログアウトを実装してみた

FlutterでAndroidでのAuth0のログイン・ログアウトを実装してみた

Clock Icon2025.01.10

こんにちは、ゲームソリューション部のsoraです。
今回は、FlutterでAndroidでのAuth0のログイン・ログアウトを実装してみたことについて書いていきます。

主に利用するパッケージ

flutter_appauth
Auth0のログイン・ログアウトを実装するために必要
https://pub.dev/packages/flutter_appauth
※公式パッケージとしてflutter_auth0がありますが、リダイレクトURLの設定が上手くいかなかったため、今回は使用しません。(ちなみにlikesも公式の方が少なかったです。)
https://pub.dev/packages/flutter_auth0

flutter_riverpod
Riverpodを使用するために必要
https://pub.dev/packages/flutter_riverpod

今回使用したpubspec.yamlの関係する部分を一部抜粋して記載します。

pubspec.yaml
# (一部抜粋)
dependencies:
  flutter:
    sdk: flutter
  flutter_appauth:
  http:
  flutter_riverpod:
  url_launcher:
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

Auth0での設定

Auth0にてApplicationsからアプリケーションを作成します。Application TypeはNativeです。
Application URLsのAllowed Callback URLsとAllowed Logout URLsには、{scheme}://callbackを入れます。
(今回の私のアプリだと、com.example.appauth2://callbackになります。)

またConnectionsのDatabaseタブで、使用するデータベースやソーシャルログインを指定できるため、必要に応じて設定します。
今回は、Auth0内のDatabaseを作成して使用することにしました。

Flutterコード

Flutterのコードは以下です。
ログインログアウトのボタンが2つある簡単な画面です。
Auth0の設定部分で指定している_domainなどは、Auth0の管理画面から確認できます。
AuthorizationTokenRequestのscopesは、このアプリがどの情報にアクセスする可能かの許可範囲を指定しています。

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

final authProvider = Provider<AuthService>((ref) => AuthService());

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

class AuthService {
  final FlutterAppAuth _appAuth = FlutterAppAuth();

  // Auth0の設定
  final String _clientId = '{clientId}';
  final String _domain = '{domain}';
  // schemeはbuild.gradleのnamespaceにある"com.example.test"のような形式
  final String _redirectUri = 'com.example.appauth2://callback';
  final String _issuer = 'https://{$_domain}';

  Future<void> login() async {
    try {
      final AuthorizationTokenResponse? result =
        await _appAuth.authorizeAndExchangeCode(
          AuthorizationTokenRequest(
            _clientId,
            _redirectUri,
            issuer: _issuer,
            scopes: ['openid', 'profile', 'email'],
          ),
        );
      if (result != null) {
        final userInfoResponse = await http.get(
          Uri.https('$_domain', '/userinfo'),
          headers: {'Authorization': 'Bearer ${result.accessToken}'},
        );
        final userInfo = jsonDecode(userInfoResponse.body);
        String? email = userInfo['email'];
        print('ログイン成功: $email');
      } else {
        print('結果がnull');
      }
    } catch (e) {
      print('ログインエラー: $e');
    }
  }

  Future<void> logout() async {
    try {
      final url = Uri.https(
        '$_domain',
        '/v2/logout',
        {
          'returnTo': _redirectUri,
          'client_id': _clientId,
        },
      );
      if (await canLaunchUrl(url)) {
        await launchUrl(url, mode: LaunchMode.externalApplication);
        print('ログアウト成功');
      } else {
          throw 'ログアウトURLを開けませんでした';
      }
    } catch (e) {
      print('ログアウトエラー: $e');
    }
  }
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Auth0 test',
      home: HomePage(),
    );
  }
}

class HomePage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final auth = ref.read(authProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Auth0 test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                await auth.login();
              },
              child: Text('ログイン'),
            ),
            ElevatedButton(
              onPressed: () async {
                await auth.logout();
              },
              child: Text('ログアウト'),
            ),
          ],
        ),
      ),
    );
  }
}

その他設定の修正

リダイレクト先のScheme設定

{project_name}/android/app/build.gradleflutter_appauthでのリダイレクト先のSchemeを設定します。

build.gradle
defaultConfig {
  applicationId = "com.example.appauth2"
  minSdk = flutter.minSdkVersion
  targetSdk = flutter.targetSdkVersion
  versionCode = flutter.versionCode
  versionName = flutter.versionName
  // この部分を追加
  manifestPlaceholders += [
      'appAuthRedirectScheme': 'com.example.appauth2'
  ]
}

ディープリンクの設定

{project_name}/android/app/src/main/AndroidManifest.xmlに、アプリに外部からアクセスするときの入り口を設定します。

AndroidManifest.xml
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data
      android:scheme="com.example.appauth2"
      android:host="callback"
      android:path="/" />
  </intent-filter>

android:path="/"を入れた場合、このアプリだとcom.example.appauth2://callback/の完全一致が必要になります。
android:path="/"を削除した場合、com.example.appauth2://callbackcom.example.appauth2://callback/anythingなども受け付けるようになります。

後者の場合だと、他のアプリでも同じURLパターンを受け付ける可能性があり、アプリの挙動としてログインログアウト後にどのアプリで開くかの確認が入りました。
sr-flutter-auth0-04

taskAffinityの削除

taskAffinityは、アクティビティのタスクスタックの所属を指定するものです。
デフォルトである空文字指定だと、アクティビティが独立したタスクを持つように強制されて、他のアクティビティとタスクを共有しないようになります。
今回のアプリでAuth0での認証を入れると、「ブラウザで認証⇒アプリに戻る」という流れになり、タスクの切り替えがうまくいかなくなります。
そのため、AndroidManifest.xmlにあるandroid:taskAffinity=""を削除します。

(参考)
https://github.com/MaikuB/flutter_appauth/issues/541

動作確認

コード作成と設定修正が終わったら、Androidでの動作確認をします。
ログインボタンを押すと、Auth0のログイン画面に遷移して、ログインできることが確認できました。
ログアウトボタンを押して、ログアウトできることも確認できました。
sr-flutter-auth0-05

以下Flutterのログです。
Warningが出ていますが、ログアウトに成功して状態が存在しないことを示しているため正常です。

I/flutter (10046): ログイン成功: {emailアドレス}
I/UrlLauncher(10046): component name for https://{domain}/v2/logout?returnTo=com.example.appauth2...
I/flutter (10046): ログアウト成功
W/AppAuth (10046): No stored state - unable to handle response

最後に

今回は、FlutterでAndroidでのAuth0のログイン・ログアウトを実装してみたことを記事にしました。
どなたかの参考になると幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.