wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

FlutterでFirebase Authenticationを実装①

4月は3週間の海外旅行に行っていたので久々の更新。

FlutterでFirebase Authenticationを実装してみたので記載。今回は純粋なアプリのログインのみ実装。 次回の記事でbackendの接続とアプリの状態保持を記載する予定。

PR

github.com

使用パッケージ

実装

まず、Firebase接続の事前準備が必要なので以下の記事を参考にinfo. plistを追加 & 修正

qiita.com

次に以下のコマンドでFirebaseの接続情報を追加

$ firebase login
$ dart pub global activate flutterfire_cli

接続したいFirebaseのプロジェクトを指定すると以下のファイルが追加される(接続情報が載っているのでGitのcommitからは除外)

■ lib/firebase_options.dart

// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
    show defaultTargetPlatform, kIsWeb, TargetPlatform;

/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
///   options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      return web;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return android;
      case TargetPlatform.iOS:
        return ios;
      case TargetPlatform.macOS:
        return macos;
      case TargetPlatform.windows:
        return windows;
      case TargetPlatform.linux:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for linux - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      default:
        throw UnsupportedError(
          'DefaultFirebaseOptions are not supported for this platform.',
        );
    }
  }

  static const FirebaseOptions web = FirebaseOptions(
    apiKey: '******'',
    appId: '******'',
    messagingSenderId: '******'',
    projectId: '******',
    authDomain: '******',
    storageBucket: '******',
    measurementId: '******',
  );

(略)

上記のコードを使用してアプリ起動時ににFirebaseの初期化の処理を追加

lib/main.dart

import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  await initHiveForFlutter();

  runApp(const MyApp());
}

これで準備完了なので残りはコードを追加
まずログイン画面の実装

lib/app/login/page.dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:sampleflutter/components/appBar/common.dart';
import 'package:sampleflutter/components/button/button.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sampleflutter/utils/auth.dart';

class Login extends HookWidget {
  const Login({Key? key}) : super(key: key);

  static final googleLogin = GoogleSignIn(scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ]);

  @override
  Widget build(BuildContext context) {
    onPressed() async {
      try {
        //Google認証フローを起動する
        final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
        //リクエストから認証情報を取得する
        final googleAuth = await googleUser?.authentication;
        //firebaseAuthで認証を行う為、credentialを作成
        final credential = GoogleAuthProvider.credential(
          accessToken: googleAuth?.accessToken,
          idToken: googleAuth?.idToken,
        );
        //作成したcredentialを元にfirebaseAuthで認証を行う
        UserCredential userCredential =
            await FirebaseAuth.instance.signInWithCredential(credential);

        if (userCredential.additionalUserInfo!.isNewUser) {
          //新規ユーザーの場合の処理
          debugPrint("新規ユーザー");
        } else {
          //既存ユーザーの場合の処理
          debugPrint("既存ユーザー");
        }

        final AuthService authService = AuthService();
        await authService.refreshAndStoreToken();
      } on FirebaseException catch (e) {
        debugPrint(e.message);
      } catch (e) {
        print(e);
      }
    }

    onLogout() async {
      final AuthService authService = AuthService();
      await authService.deleteToken();

      await FirebaseAuth.instance.signOut();
    }

    return Scaffold(
        appBar: const CommonAppBar(title: "ログイン"),
        body: Center(
            child: StreamBuilder(
                stream: FirebaseAuth.instance.authStateChanges(),
                builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
                  if (!snapshot.hasData) {
                    return Button(
                        title: "Google ログイン", width: 300, onPressed: onPressed);
                  } else {
                    return Button(
                        title: "ログアウト", width: 300, onPressed: onLogout);
                  }
                })));
  }
}

これで以下みたいにログインの実装が可能

www.youtube.com

後はGraphQLに認証トークンを追加する場合は以下にように実装 まず、Auth用のServiceを追加

lib/utils/auth.dart

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final FlutterSecureStorage secureStorage = const FlutterSecureStorage();

  // トークンを取得して安全に保存する
  Future<void> refreshAndStoreToken() async {
    User? user = _auth.currentUser;
    if (user != null) {
      String? token = await user.getIdToken(true);
      await secureStorage.write(key: 'token', value: token);
      await secureStorage.write(
          key: 'tokenDate', value: DateTime.now().toIso8601String());
    }
  }

  // トークンの有効性をチェックし、必要に応じて更新
  Future<String?> getToken() async {
    try {
      String? token = await secureStorage.read(key: 'token');
      if (token == null) {
        return null;
      }
      String? storedDate = await secureStorage.read(key: 'tokenDate');
      if (storedDate == null) {
        return null;
      }

      DateTime tokenDate = DateTime.parse(storedDate);
      DateTime now = DateTime.now();

      // トークンの有効期限を設定(Firebaseのデフォルトは約1時間)
      if (now.difference(tokenDate).inHours >= 1) {
        await refreshAndStoreToken();
        token = await secureStorage.read(key: 'token'); // 更新されたトークンを取得
      }
      debugPrint('token: $token');

      return token;
    } catch (e) {
      return null;
    }
  }

  Future<void> deleteToken() async {
    await secureStorage.delete(key: 'token');
    await secureStorage.delete(key: 'tokenDate');
  }
}

上記で追加したgetTokenを以下のメソッドに追加してHeaderに追加

lib/main.dart

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    final HttpLink httpLink =
        HttpLink('https://stock-keeper-voytob3xvq-an.a.run.app/graphql');
    final AuthService authService = AuthService();
    final authLink = AuthLink(getToken: () async => authService.getToken());
    final link = authLink.concat(httpLink);

    final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
      GraphQLClient(
        link: link,
        cache: GraphQLCache(store: InMemoryStore()),
      ),
    );

これでAPIでFirebase Authを使用する準備が整った。次回の記事でbackendの接続とアプリの状態保持を記載する予定。