Supabase Auth Dart
認証ライブラリ
Supabase Auth Dart
概要
Supabase Auth DartはFlutterアプリケーション向けのオープンソース認証ライブラリです。Supabase BaaS(Backend as a Service)プラットフォームの一部として、メール認証、ソーシャルログイン、多要素認証、リアルタイム認証状態管理など包括的な認証機能を提供します。PostgreSQLデータベースとの完全統合により、スケーラブルで安全な認証システムを構築できます。
詳細
Supabase Auth Dartは2024年現在、Flutter開発における最も包括的な認証ソリューションの一つです。Googleアカウント、Apple ID、GitHub、Discord、Facebookなど20以上のOAuthプロバイダーをサポートし、メール認証、電話番号認証、マジックリンク認証に対応しています。JWT(JSON Web Token)ベースの認証を採用し、リアルタイム認証状態変更の監視、セッション管理、多要素認証(MFA)、行レベルセキュリティ(RLS)によるデータアクセス制御が可能です。Flutter Web、iOS、Android、macOS、Windows、Linuxなど全プラットフォームをサポートしており、特にFlutter 3.0以降で最適化されています。
主要機能
- Flutter完全統合: Flutter 3.0+対応の公式パッケージ
- 多様な認証方式: OAuth、メール、電話、マジックリンク対応
- リアルタイム状態管理: 認証状態変更の即座な検知
- 多要素認証: TOTP、SMS、メール2段階認証対応
- セッション管理: 自動セッション更新とストレージ
- 行レベルセキュリティ: PostgreSQLベースの細かいアクセス制御
- ディープリンク対応: モバイルアプリの認証フロー最適化
- カスタマイズ可能UI: 事前構築済みウィジェットと完全カスタマイズ
サポートプラットフォーム
- モバイル: iOS、Android
- デスクトップ: macOS、Windows、Linux
- Web: Progressive Web App(PWA)対応
- マルチプラットフォーム: 単一コードベースで全プラットフォーム対応
サポート認証プロバイダー
- ソーシャル: Google、Apple、Facebook、GitHub、Discord、Twitter等20+プロバイダー
- メール/パスワード: 従来型の認証
- マジックリンク: パスワードレス認証
- 電話番号: SMS認証
- 匿名認証: ゲスト機能実装
メリット・デメリット
メリット
- 完全なFlutter統合: Flutter専用設計で開発効率最大化
- オープンソース: MIT ライセンスで商用利用可能
- スケーラビリティ: Supabaseインフラによる自動スケーリング
- リアルタイム機能: WebSocket基盤のリアルタイム認証状態
- 包括的セキュリティ: JWT + RLS による強固なセキュリティ
- ゼロ設定: 最小限の設定で本格認証システム構築
- 豊富なドキュメント: 日本語対応含む充実したドキュメント
- アクティブ開発: Supabaseチームによる継続的な開発とサポート
デメリット
- ベンダーロックイン: Supabaseエコシステム依存
- ネットワーク依存: オフライン時の機能制限
- 料金体系: 大規模利用時のコスト(無料枠は充実)
- 学習コスト: Supabase全体のエコシステム理解が必要
- カスタマイズ制限: Supabaseサーバー側の制約
- データ所在地: サーバー地域選択の制約
参考ページ
- Supabase Flutter Documentation - 公式Flutter統合ガイド
- Supabase Auth UI Flutter - 認証UIコンポーネント
- supabase_flutter | pub.dev - 公式パッケージ
- Supabase GitHub - Flutter - ソースコードとサンプル
- Flutter Authentication with Supabase - 公式ブログ
- Supabase Dashboard - プロジェクト管理
書き方の例
基本セットアップ
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
supabase_flutter: ^2.0.0
supabase_auth_ui: ^0.1.0
dev_dependencies:
flutter_test:
sdk: flutter
初期化とプロジェクト設定
// main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
authOptions: const FlutterAuthClientOptions(
authFlowType: AuthFlowType.pkce,
),
);
runApp(MyApp());
}
final supabase = Supabase.instance.client;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Supabase Auth Demo',
home: AuthGate(),
);
}
}
認証状態管理
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthGate extends StatefulWidget {
@override
_AuthGateState createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
User? _user;
@override
void initState() {
super.initState();
_getInitialSession();
_listenToAuthChanges();
}
Future<void> _getInitialSession() async {
final session = supabase.auth.currentSession;
setState(() {
_user = session?.user;
});
}
void _listenToAuthChanges() {
supabase.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent event = data.event;
final Session? session = data.session;
setState(() {
_user = session?.user;
});
switch (event) {
case AuthChangeEvent.signedIn:
print('User signed in: ${_user?.email}');
break;
case AuthChangeEvent.signedOut:
print('User signed out');
break;
case AuthChangeEvent.passwordRecovery:
print('Password recovery initiated');
break;
case AuthChangeEvent.tokenRefreshed:
print('Token refreshed');
break;
case AuthChangeEvent.userUpdated:
print('User updated');
break;
}
});
}
@override
Widget build(BuildContext context) {
return _user == null ? LoginPage() : HomePage();
}
}
メール認証とパスワード
class EmailPasswordAuth extends StatefulWidget {
@override
_EmailPasswordAuthState createState() => _EmailPasswordAuthState();
}
class _EmailPasswordAuthState extends State<EmailPasswordAuth> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
Future<void> _signUp() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final response = await supabase.auth.signUp(
email: _emailController.text.trim(),
password: _passwordController.text,
data: {
'display_name': 'User Name',
'avatar_url': 'https://example.com/avatar.jpg',
},
);
if (response.user != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('確認メールを送信しました')),
);
}
} on AuthException catch (error) {
setState(() {
_errorMessage = error.message;
});
} catch (error) {
setState(() {
_errorMessage = '予期しないエラーが発生しました';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _signIn() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final response = await supabase.auth.signInWithPassword(
email: _emailController.text.trim(),
password: _passwordController.text,
);
if (response.user != null) {
// ログイン成功 - AuthGateが自動的にHomePageに切り替える
}
} on AuthException catch (error) {
setState(() {
_errorMessage = error.message;
});
} catch (error) {
setState(() {
_errorMessage = 'ログインに失敗しました';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('認証')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'メールアドレス'),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'パスワード'),
obscureText: true,
),
SizedBox(height: 16),
if (_errorMessage != null)
Text(_errorMessage!, style: TextStyle(color: Colors.red)),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator()
: Column(
children: [
ElevatedButton(
onPressed: _signUp,
child: Text('新規登録'),
),
ElevatedButton(
onPressed: _signIn,
child: Text('ログイン'),
),
],
),
],
),
),
);
}
}
OAuth認証(Google、Apple等)
class SocialAuth extends StatelessWidget {
Future<void> _signInWithGoogle() async {
try {
await supabase.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo: kIsWeb ? null : 'io.supabase.flutterdemo://callback',
authScreenLaunchMode: LaunchMode.externalApplication,
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Google認証エラー: ${error.message}')),
);
}
}
Future<void> _signInWithApple() async {
try {
await supabase.auth.signInWithOAuth(
OAuthProvider.apple,
redirectTo: kIsWeb ? null : 'io.supabase.flutterdemo://callback',
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Apple認証エラー: ${error.message}')),
);
}
}
Future<void> _signInWithGitHub() async {
try {
await supabase.auth.signInWithOAuth(
OAuthProvider.github,
redirectTo: kIsWeb ? null : 'io.supabase.flutterdemo://callback',
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('GitHub認証エラー: ${error.message}')),
);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton.icon(
icon: Icon(Icons.account_circle),
label: Text('Googleでログイン'),
onPressed: _signInWithGoogle,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
ElevatedButton.icon(
icon: Icon(Icons.apple),
label: Text('Appleでログイン'),
onPressed: _signInWithApple,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
),
ElevatedButton.icon(
icon: Icon(Icons.code),
label: Text('GitHubでログイン'),
onPressed: _signInWithGitHub,
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[800]),
),
],
);
}
}
マジックリンク認証
class MagicLinkAuth extends StatefulWidget {
@override
_MagicLinkAuthState createState() => _MagicLinkAuthState();
}
class _MagicLinkAuthState extends State<MagicLinkAuth> {
final _emailController = TextEditingController();
bool _isLoading = false;
Future<void> _sendMagicLink() async {
setState(() {
_isLoading = true;
});
try {
await supabase.auth.signInWithOtp(
email: _emailController.text.trim(),
emailRedirectTo: kIsWeb
? null
: 'io.supabase.flutterdemo://callback',
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('マジックリンクを送信しました。メールをご確認ください。'),
backgroundColor: Colors.green,
),
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('エラー: ${error.message}'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('マジックリンク認証')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
helperText: 'ログイン用のマジックリンクを送信します',
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _sendMagicLink,
child: Text('マジックリンクを送信'),
),
],
),
),
);
}
}
多要素認証(MFA)
class MFASetup extends StatefulWidget {
@override
_MFASetupState createState() => _MFASetupState();
}
class _MFASetupState extends State<MFASetup> {
String? _factorId;
String? _qrCode;
String? _secret;
final _totpController = TextEditingController();
Future<void> _enrollMFA() async {
try {
final response = await supabase.auth.mfa.enroll(
issuer: 'MyApp',
friendlyName: 'MyApp MFA',
);
setState(() {
_factorId = response.id;
_qrCode = response.totp?.qrCode;
_secret = response.totp?.secret;
});
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('MFA登録エラー: $error')),
);
}
}
Future<void> _verifyMFA() async {
if (_factorId == null) return;
try {
// チャレンジの準備
final challengeResponse = await supabase.auth.mfa.challenge(
factorId: _factorId!,
);
// TOTPコードの検証
final verifyResponse = await supabase.auth.mfa.verify(
factorId: _factorId!,
challengeId: challengeResponse.id,
code: _totpController.text,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('MFA認証成功'),
backgroundColor: Colors.green,
),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('MFA認証エラー: $error')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('多要素認証設定')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
if (_qrCode == null) ...[
ElevatedButton(
onPressed: _enrollMFA,
child: Text('MFA登録開始'),
),
] else ...[
Text('認証アプリでQRコードをスキャンしてください:'),
SizedBox(height: 16),
// QRコードの表示(qr_flutter パッケージ使用)
Container(
width: 200,
height: 200,
color: Colors.grey[300],
child: Center(child: Text('QRコード表示エリア')),
),
SizedBox(height: 16),
Text('シークレットキー: $_secret'),
SizedBox(height: 16),
TextField(
controller: _totpController,
decoration: InputDecoration(
labelText: '認証アプリの6桁コード',
),
keyboardType: TextInputType.number,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _verifyMFA,
child: Text('MFA認証'),
),
],
],
),
),
);
}
}
ユーザープロフィール管理
class UserProfile extends StatefulWidget {
@override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
final _displayNameController = TextEditingController();
final _websiteController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadUserData();
}
void _loadUserData() {
final user = supabase.auth.currentUser;
if (user != null) {
_displayNameController.text = user.userMetadata?['display_name'] ?? '';
_websiteController.text = user.userMetadata?['website'] ?? '';
}
}
Future<void> _updateProfile() async {
setState(() {
_isLoading = true;
});
try {
await supabase.auth.updateUser(
UserAttributes(
data: {
'display_name': _displayNameController.text,
'website': _websiteController.text,
},
),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('プロフィールを更新しました'),
backgroundColor: Colors.green,
),
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更新エラー: ${error.message}')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _signOut() async {
await supabase.auth.signOut();
}
@override
Widget build(BuildContext context) {
final user = supabase.auth.currentUser;
return Scaffold(
appBar: AppBar(
title: Text('プロフィール'),
actions: [
IconButton(
icon: Icon(Icons.logout),
onPressed: _signOut,
),
],
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
CircleAvatar(
radius: 50,
backgroundImage: user?.userMetadata?['avatar_url'] != null
? NetworkImage(user!.userMetadata!['avatar_url'])
: null,
child: user?.userMetadata?['avatar_url'] == null
? Icon(Icons.person, size: 50)
: null,
),
SizedBox(height: 16),
Text('メール: ${user?.email ?? 'N/A'}'),
SizedBox(height: 16),
TextField(
controller: _displayNameController,
decoration: InputDecoration(labelText: '表示名'),
),
TextField(
controller: _websiteController,
decoration: InputDecoration(labelText: 'ウェブサイト'),
),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _updateProfile,
child: Text('プロフィール更新'),
),
],
),
),
);
}
}
ディープリンク設定
// android/app/src/main/AndroidManifest.xml
/*
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">
<!-- Standard App Link -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="yourdomain.com" />
</intent-filter>
<!-- Custom URL Scheme -->
<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="io.supabase.flutterdemo" />
</intent-filter>
</activity>
*/
// iOS設定 (ios/Runner/Info.plist)
/*
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>supabase-auth</string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.supabase.flutterdemo</string>
</array>
</dict>
</array>
</dict>
*/
カスタムローカルストレージ
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class SecureLocalStorage extends LocalStorage {
final FlutterSecureStorage _storage = FlutterSecureStorage();
@override
Future<void> initialize() async {}
@override
Future<String?> accessToken() async {
return await _storage.read(key: supabasePersistSessionKey);
}
@override
Future<bool> hasAccessToken() async {
return await _storage.containsKey(key: supabasePersistSessionKey);
}
@override
Future<void> persistSession(String persistSessionString) async {
await _storage.write(
key: supabasePersistSessionKey,
value: persistSessionString,
);
}
@override
Future<void> removePersistedSession() async {
await _storage.delete(key: supabasePersistSessionKey);
}
}
// 使用例
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
authOptions: FlutterAuthClientOptions(
localStorage: SecureLocalStorage(),
),
);