FlutterでSupabase Authを使ってGoogle認証を実装する
Flutter+SupabaseでGoogle認証
SupabaseはFirebaseの代替と謳われている BaaS (Backend As A Service) で、Postgresデータベース、認証、ファイルストレージ、インスタントAPIなど多数のサービスが提供されています。
今回は、SupabaseのAuthenticationの機能を使って、FlutterアプリにGoogleアカウントでログインするボタンを実装する方法を紹介します。
以下のガイドを参考に進めていきます。
前提
- 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
クライアントIDを作成する
Google CloudでGoogle認証に必要なクライアントIDを作成します。
Web、iOS、Androidそれぞれに対してOAuth認証情報を設定する必要があります。
使用するGoogle Cloudプロジェクトで「APIとサービス」→「認証情報」を選択します。
次に「+認証情報を作成」→「OAuthクライアントID」を選択します。
Web, macOS, Windows, Linux
- 「アプリケーションの種類」で「ウェブアプリケーション」を選択します。
- 任意の名前を設定します。
- 作成を選択します。
iOS
追加で認証情報を作成していきます。
- 「アプリケーションの種類」で「iOS」を選択します。
- 任意の名前を設定します。
- バンドルIDを入力します。
バンドルIDはios/Runner.xcodeproj/project.pbxproj
内にある、PRODUCT_BUNDLE_IDENTIFIER
の値を設定します。 - 作成を選択します。
Android
もうひとつ認証情報を作成します。
- 「アプリケーションの種類」で「Android」を選択します。
- 任意の名前を設定します。
- パッケージ名を入力します。
パッケージ名は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" // ここ ...
- SHA-1証明書のフィンガープリントを入力します。
これはデバッグ用証明書のためリリース版からの認証はできません。
必要であれば、デバッグとリリース用に2つ認証情報を用意するのをおすすめします。
以下のコマンドを入力して、デバッグ用のフィンガープリントを取得します。# debug.keystoreのパスはWindowsの場合、デフォルトで # C:\Users\ユーザー名\.android\debug.keystore です。 # パスワードはデフォルトで android です。 keytool -keystore <debug.keystoreのパス> -list -v
- 作成を選択します。
Supabaseの設定
Supabaseで認証関連の設定とリダイレクトURLの設定をしていきます。
Auth Providers
- 「Authentication」→「Providers」から Google を開きます。
- 「Enable Sign in with Google」を有効化します。
- 「Client ID (for OAuth)」にGCPで作成したWeb用のクライアントIDを入力します。
- 「Client Secret (for OAuth)」にWeb用のクライアントシークレットを入力します。
- 「Authorized Client IDs (for Android, One Tap, and Chrome extensions) にAndroid用のクライアントIDとiOS用のクライアントIDをカンマ区切りで入力します。
- 「Skip nonce checks」はiOSでGoogle認証を利用する場合に有効化します。
URL Configuration
- 「Authentication」→「URL Configuration」を選択。
- 「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 URL
と Project API Keys
にある anon
public
のキーをコピーしておいてください。
.envファイルを作成
GitHubなどで公開する場合、APIキーなどをコードに直接貼るのはマズいので、別ファイルに環境変数として値を置いて .gitignore
で公開されないように設定します。
プロジェクトのルートに .env
ファイルを作成します。
以下のようにSupabaseのURLや、GCPのクライアントIDを書き込みます。
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も追加します。
# environment
*.env
*env.g.dart
enviedで環境変数を使用可能にする
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
を生成します。
dart run build_runner build --delete-conflicting-outputs
生成されたファイルを見てみると、obfuscateを指定した値は難読化されていて、指定していないものはそのままなのがわかります。
この env.g.dart
は .env
同様、公開してはいけません。
Supabaseの処理
Supabaseの初期化処理を追加します。
ここでSupabaseのURLと、APIキーを使用します。
Env.<env.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
に格納します。
final supabase = Supabase.instance.client;
認証が成功した時に認証セッションのユーザーIDを格納するコードを追加します。
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用のコードを追加します。
// 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
メソッドを利用して認証します。
...
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等での設定が必要ですが、ここでは省略します。
...
<!-- 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