🔑

FlutterでSupabase Authを使ってGoogle認証を実装する

2024/11/08に公開

Flutter+SupabaseでGoogle認証

SupabaseはFirebaseの代替と謳われている BaaS (Backend As A Service) で、Postgresデータベース、認証、ファイルストレージ、インスタントAPIなど多数のサービスが提供されています。
今回は、SupabaseのAuthenticationの機能を使って、FlutterアプリにGoogleアカウントでログインするボタンを実装する方法を紹介します。

以下のガイドを参考に進めていきます。
https://supabase.com/docs/guides/auth/social-login/auth-google?queryGroups=platform&platform=flutter

前提

  • Flutterの環境を構築済み
  • Flutterのプロジェクトを作成済み
  • Supabaseの登録とプロジェクトを作成済み
  • Google Cloudのプロジェクトを作成済み

パッケージを追加

必要なパッケージをインストールします。
以下のコマンドを実行してください。

flutter pub add google_sign_in
flutter pub add supabase_flutter
flutter pub add envied
flutter pub add --dev envied_generator
flutter pub add --dev build_runner

https://pub.dev/packages/google_sign_in
https://pub.dev/packages/supabase_flutter
https://pub.dev/packages/envied

クライアントIDを作成する

Google CloudでGoogle認証に必要なクライアントIDを作成します。
Web、iOS、Androidそれぞれに対してOAuth認証情報を設定する必要があります。
使用するGoogle Cloudプロジェクトで「APIとサービス」→「認証情報」を選択します。

次に「+認証情報を作成」→「OAuthクライアントID」を選択します。

OAuth クライアント ID を作成するには、まず同意画面で設定を行う必要があります
と表示された場合は、同意画面の構成→User Typeを外部にして作成してください。

Web, macOS, Windows, Linux

  1. 「アプリケーションの種類」で「ウェブアプリケーション」を選択します。
  2. 任意の名前を設定します。
  3. 作成を選択します。

iOS

追加で認証情報を作成していきます。

  1. 「アプリケーションの種類」で「iOS」を選択します。
  2. 任意の名前を設定します。
  3. バンドルIDを入力します。
    バンドルIDは ios/Runner.xcodeproj/project.pbxproj 内にある、 PRODUCT_BUNDLE_IDENTIFIER の値を設定します。
  4. 作成を選択します。

Android

もうひとつ認証情報を作成します。

  1. 「アプリケーションの種類」で「Android」を選択します。
  2. 任意の名前を設定します。
  3. パッケージ名を入力します。
    パッケージ名は android/app/build.gradle 内にある applicationId の値を設定します。
    build.gradle
    ...
    defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId = "com.example.app" // ここ
    ...
    
  4. SHA-1証明書のフィンガープリントを入力します。
    これはデバッグ用証明書のためリリース版からの認証はできません。
    必要であれば、デバッグとリリース用に2つ認証情報を用意するのをおすすめします。
    以下のコマンドを入力して、デバッグ用のフィンガープリントを取得します。
    # debug.keystoreのパスはWindowsの場合、デフォルトで
    # C:\Users\ユーザー名\.android\debug.keystore です。
    # パスワードはデフォルトで android です。
    keytool -keystore <debug.keystoreのパス> -list -v
    
  5. 作成を選択します。

Supabaseの設定

Supabaseで認証関連の設定とリダイレクトURLの設定をしていきます。

Auth Providers

  1. 「Authentication」→「Providers」から Google を開きます。
  2. 「Enable Sign in with Google」を有効化します。
  3. 「Client ID (for OAuth)」にGCPで作成したWeb用のクライアントIDを入力します。
  4. 「Client Secret (for OAuth)」にWeb用のクライアントシークレットを入力します。
  5. 「Authorized Client IDs (for Android, One Tap, and Chrome extensions) にAndroid用のクライアントIDiOS用のクライアントIDをカンマ区切りで入力します。
  6. 「Skip nonce checks」はiOSでGoogle認証を利用する場合に有効化します。

URL Configuration

  1. 「Authentication」→「URL Configuration」を選択。
  2. 「Site URL」に、WebでFlutterアプリを開くときのURLを設定します。
    Webからログインし、認証が成功するとデフォルトでSite URLにリダイレクトされます。
    ここでは http://localhost:56000/ とします。

GoogleCloud側でURLの設定

Webでのログイン後にリダイレクトするURIを設定します。
Supabaseの「Authentication」→「Providers」から「Callback URL (for OAuth)」に表示されているURLをコピーします。

GCPでWeb用のクライアントIDを選択し、「承認済みのリダイレクトURI」に先ほどコピーしたURLを貼り付けます。

Flutterで実装する

GCPとSupabaseでの設定が終わったら、いよいよコードを書いていきます。
コードエディタ側ではSupabaseのプロジェクトURLとAPIキーも使うのでこちらも用意しておきます。
「Project Settings」→「API」を選択します。
Project URLProject API Keys にある anon public のキーをコピーしておいてください。

.envファイルを作成

GitHubなどで公開する場合、APIキーなどをコードに直接貼るのはマズいので、別ファイルに環境変数として値を置いて .gitignore で公開されないように設定します。
プロジェクトのルートに .env ファイルを作成します。
以下のようにSupabaseのURLや、GCPのクライアントIDを書き込みます。

.env
PUBLIC_SUPABASE_URL="(SupabaseのURL)"
PUBLIC_SUPABASE_ANON_KEY="(SupabaseのAPI Key)"
WEB_CLIENT_ID="(Web用クライアントID)"
IOS_CLIENT_ID="(iOS用クライアントID)"
ANDROID_CLIENT_ID="(Android用クライアントID)"
# hoge.huga.co なら co.huga.hoge
REVERSED_CLIENT_ID="(ドメインを逆順にしたiOS用クライアントID)"

.gitignoreを編集

.envが設定されていて、gitの管理対象になっていないか必ず確認してください。
このあと設定するenviedで生成されるenv.g.dartも追加します。

.gitignore
# environment
*.env
*env.g.dart

enviedで環境変数を使用可能にする

lib フォルダ内に、env フォルダを作成します。
作成したフォルダに env.dart ファイルを追加して以下のコードを記入してください。

lib/env/env.dart
import 'package:envied/envied.dart';

part 'env.g.dart';

(path: '.env')
abstract class Env {
  (varName: 'PUBLIC_SUPABASE_URL', obfuscate: true)
  static String supabaseUrl = _Env.supabaseUrl;

  (varName: 'PUBLIC_SUPABASE_ANON_KEY', obfuscate: true)
  static String anonKey = _Env.anonKey;

  (varName: 'WEB_CLIENT_ID')
  static String webClientId = _Env.webClientId;

  (varName: 'IOS_CLIENT_ID')
  static String iosClientId = _Env.iosClientId;

  (varName: 'ANDROID_CLIENT_ID')
  static String androidClientId = _Env.androidClientId;

  (varName: 'REVERSED_CLIENT_ID')
  static String reversedClientId = _Env.reversedClientId;
}

念のため、SupabaseのURLとAnonKeyは obfuscate: true で難読化をしています。
コードが入力できたら、以下のコマンドを実行して env.g.dart を生成します。

flutter clean
dart run build_runner build --delete-conflicting-outputs

生成されたファイルを見てみると、obfuscateを指定した値は難読化されていて、指定していないものはそのままなのがわかります。
この env.g.dart.env 同様、公開してはいけません。

Supabaseの処理

Supabaseの初期化処理を追加します。
ここでSupabaseのURLと、APIキーを使用します。
Env.<env.dartで設定した変数名> で値を取得できます。

lib/main.dart
// 追加
import 'package:supabase_flutter/supabase_flutter.dart';
// 追加:作成したenv.dartをインポートします
import 'package:<プロジェクト名>/env/env.dart';

void main() async {
  //--- 追加 ---//
  await Supabase.initialize(
    url: Env.supabaseUrl,
    anonKey: Env.anonKey,
  );
  //-----------//
  runApp(const MyApp());
}

Supabaseのクライアントを supabase に格納します。

lib/main.dart
final supabase = Supabase.instance.client;

認証が成功した時に認証セッションのユーザーIDを格納するコードを追加します。

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  String? _userId; // 追加

  //--- 追加 ---//
  
  void initState() {
    super.initState();
    supabase.auth.onAuthStateChange.listen((data) {
      setState(() {
        _userId = data.session?.user.id;
      });
    });
  }
  //-----------//
...

Googleログイン処理

ログインの処理を追加します。
Android/iOSアプリとWebでは実装方法が異なります。
Android/iOSの場合は google_sign_in パッケージを使用して実装します。
Webの場合は signInWithOAuth メソッドを使います。

まず、Android/iOS用のコードを追加します。

lib/main.dart
// import文の追加
import 'package:google_sign_in/google_sign_in.dart';

// Android, iOS の場合のGoogleログイン処理
Future<void> _nativeGoogleSignIn() async {
  final GoogleSignIn googleSignIn = GoogleSignIn(
    clientId: Platform.isAndroid ? Env.androidClientId : Env.iosClientId,
    serverClientId: Env.webClientId,
  );
  final googleUser = await googleSignIn.signIn();
  final googleAuth = await googleUser!.authentication;
  final accessToken = googleAuth.accessToken;
  final idToken = googleAuth.idToken;

  if (accessToken == null) {
    throw 'No Access Token found.';
  }
  if (idToken == null) {
    throw 'No ID Token found.';
  }

  await supabase.auth.signInWithIdToken(
    provider: OAuthProvider.google,
    idToken: idToken,
    accessToken: accessToken,
  );
}

続いて、アプリの画面を作成します。
ログインボタンを押した時に、Android/iOSアプリかWebかを判定し、Webの場合は signInWithOAuth メソッドを利用して認証します。

lib/main.dart
...

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          if (_userId == null) const Text('Not signed in'),
          if (_userId == null) ElevatedButton(
            onPressed: () async {
              if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
                // Android, iOS の場合
                await _nativeGoogleSignIn();
              } else {
                // Webブラウザの場合
                await supabase.auth.signInWithOAuth(OAuthProvider.google);
              }
            },
            child: const Text('Google Login'),
          ),
          if (_userId != null) Text('User ID: $_userId'),
        ],
      ),
    ),
  );
}
...

iOS向けの設定

iOSの場合は ios/Runner/Info.plist の設定を追加する必要があります。
REVERSED_CLIENT_ID の値を .env から読み込めるようにするにはさらにXcode等での設定が必要ですが、ここでは省略します。

ios/Runner/Info.plist
...
<!-- Put me in the [my_project]/ios/Runner/Info.plist file -->
<!-- Google Sign-in Section -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <!-- TODO -->
      <string>$(REVERSED_CLIENT_ID)</string>
    </array>
  </dict>
</array>
<!-- End of the Google Sign-in Section -->
...

実行結果

以上でGoogle認証をするための最低限の実装は完了です。
Webブラウザでデバッグをする場合、Supabaseで設定したSiteURLのポート番号と同じにするために、--web-port <ポート番号> を加えて実行します。

flutter run --web-port 56000

Googleでログインしていない状態では以下のように「Not signed in」と表示されます。

「Google Login」ボタンを押すとGoogleのログイン画面に飛びます。

ログインが完了すると、Supabaseの認証セッションから取得したユーザーIDが表示されます。

ソースコード全体

lib/main.dart
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:login_test/env/env.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  // supabaseの初期化
  await Supabase.initialize(
    url: Env.supabaseUrl, // supabaseのURL
    anonKey: Env.anonKey, // supabaseのプロジェクトのAPIキー(anon)
  );
  runApp(const MyApp());
}

// supabase変数にインスタンスを格納しておく
final supabase = Supabase.instance.client;

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Login',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Login Test'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String? _userId;

  // Android, iOS の場合のGoogleログイン処理
  Future<void> _nativeGoogleSignIn() async {
    final GoogleSignIn googleSignIn = GoogleSignIn(
      clientId: Platform.isAndroid ? Env.androidClientId : Env.iosClientId,
      serverClientId: Env.webClientId,
    );
    final googleUser = await googleSignIn.signIn();
    final googleAuth = await googleUser!.authentication;
    final accessToken = googleAuth.accessToken;
    final idToken = googleAuth.idToken;

    if (accessToken == null) {
      throw 'No Access Token found.';
    }
    if (idToken == null) {
      throw 'No ID Token found.';
    }

    await supabase.auth.signInWithIdToken(
      provider: OAuthProvider.google,
      idToken: idToken,
      accessToken: accessToken,
    );
  }

  
  void initState() {
    super.initState();
    supabase.auth.onAuthStateChange.listen((data) {
      setState(() {
        _userId = data.session?.user.id; // ユーザーIDの取得
      });
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            if (_userId == null) const Text('Not signed in'),
            if (_userId == null) ElevatedButton(
              onPressed: () async {
                if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
                  // Android, iOS の場合
                  await _nativeGoogleSignIn();
                } else {
                  // Webブラウザの場合
                  await supabase.auth.signInWithOAuth(OAuthProvider.google);
                }
              },
              child: const Text('Google Login'),
            ),
            if (_userId != null) Text('User ID: $_userId'),
          ],
        ),
      ),
    );
  }
}

Discussion