Supabase Auth Dart
Authentication Library
Supabase Auth Dart
Overview
Supabase Auth Dart is an open-source authentication library for Flutter applications. As part of the Supabase BaaS (Backend as a Service) platform, it provides comprehensive authentication features including email authentication, social login, multi-factor authentication, and real-time authentication state management. With complete integration with PostgreSQL database, you can build scalable and secure authentication systems.
Details
Supabase Auth Dart is one of the most comprehensive authentication solutions for Flutter development as of 2024. It supports over 20 OAuth providers including Google Account, Apple ID, GitHub, Discord, and Facebook, along with email authentication, phone number authentication, and magic link authentication. It adopts JWT (JSON Web Token) based authentication and enables real-time authentication state change monitoring, session management, multi-factor authentication (MFA), and data access control through Row Level Security (RLS). It supports all platforms including Flutter Web, iOS, Android, macOS, Windows, and Linux, with particular optimization for Flutter 3.0 and later.
Key Features
- Complete Flutter Integration: Official package supporting Flutter 3.0+
- Multiple Authentication Methods: OAuth, email, phone, magic link support
- Real-time State Management: Instant detection of authentication state changes
- Multi-Factor Authentication: TOTP, SMS, email 2FA support
- Session Management: Automatic session refresh and storage
- Row Level Security: Fine-grained PostgreSQL-based access control
- Deep Link Support: Optimized authentication flow for mobile apps
- Customizable UI: Pre-built widgets and complete customization
Supported Platforms
- Mobile: iOS, Android
- Desktop: macOS, Windows, Linux
- Web: Progressive Web App (PWA) support
- Multi-platform: Single codebase for all platforms
Supported Authentication Providers
- Social: Google, Apple, Facebook, GitHub, Discord, Twitter, and 20+ providers
- Email/Password: Traditional authentication
- Magic Link: Passwordless authentication
- Phone Number: SMS authentication
- Anonymous: Guest functionality implementation
Pros and Cons
Pros
- Complete Flutter Integration: Flutter-specific design maximizing development efficiency
- Open Source: MIT License allowing commercial use
- Scalability: Auto-scaling through Supabase infrastructure
- Real-time Functionality: WebSocket-based real-time authentication state
- Comprehensive Security: Strong security through JWT + RLS
- Zero Configuration: Build production authentication system with minimal setup
- Rich Documentation: Comprehensive documentation including Japanese support
- Active Development: Continuous development and support by Supabase team
Cons
- Vendor Lock-in: Dependency on Supabase ecosystem
- Network Dependency: Limited functionality when offline
- Pricing Structure: Cost considerations for large-scale usage (generous free tier)
- Learning Curve: Need to understand entire Supabase ecosystem
- Customization Limitations: Server-side constraints from Supabase
- Data Location: Server region selection constraints
Reference Links
- Supabase Flutter Documentation - Official Flutter integration guide
- Supabase Auth UI Flutter - Authentication UI components
- supabase_flutter | pub.dev - Official package
- Supabase GitHub - Flutter - Source code and samples
- Flutter Authentication with Supabase - Official blog
- Supabase Dashboard - Project management
Code Examples
Basic Setup
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
supabase_flutter: ^2.0.0
supabase_auth_ui: ^0.1.0
dev_dependencies:
flutter_test:
sdk: flutter
Initialization and Project Setup
// 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(),
);
}
}
Authentication State Management
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();
}
}
Email Authentication and Password
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('Confirmation email sent')),
);
}
} on AuthException catch (error) {
setState(() {
_errorMessage = error.message;
});
} catch (error) {
setState(() {
_errorMessage = 'An unexpected error occurred';
});
} 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) {
// Login successful - AuthGate will automatically switch to HomePage
}
} on AuthException catch (error) {
setState(() {
_errorMessage = error.message;
});
} catch (error) {
setState(() {
_errorMessage = 'Login failed';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Authentication')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email Address'),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
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('Sign Up'),
),
ElevatedButton(
onPressed: _signIn,
child: Text('Sign In'),
),
],
),
],
),
),
);
}
}
OAuth Authentication (Google, Apple, etc.)
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 auth error: ${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 auth error: ${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 auth error: ${error.message}')),
);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton.icon(
icon: Icon(Icons.account_circle),
label: Text('Sign in with Google'),
onPressed: _signInWithGoogle,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
ElevatedButton.icon(
icon: Icon(Icons.apple),
label: Text('Sign in with Apple'),
onPressed: _signInWithApple,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
),
ElevatedButton.icon(
icon: Icon(Icons.code),
label: Text('Sign in with GitHub'),
onPressed: _signInWithGitHub,
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[800]),
),
],
);
}
}
Magic Link Authentication
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('Magic link sent. Please check your email.'),
backgroundColor: Colors.green,
),
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${error.message}'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Magic Link Authentication')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email Address',
helperText: 'We will send you a magic link to sign in',
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _sendMagicLink,
child: Text('Send Magic Link'),
),
],
),
),
);
}
}
Multi-Factor Authentication (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 enrollment error: $error')),
);
}
}
Future<void> _verifyMFA() async {
if (_factorId == null) return;
try {
// Prepare challenge
final challengeResponse = await supabase.auth.mfa.challenge(
factorId: _factorId!,
);
// Verify TOTP code
final verifyResponse = await supabase.auth.mfa.verify(
factorId: _factorId!,
challengeId: challengeResponse.id,
code: _totpController.text,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('MFA authentication successful'),
backgroundColor: Colors.green,
),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('MFA authentication error: $error')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Multi-Factor Authentication Setup')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
if (_qrCode == null) ...[
ElevatedButton(
onPressed: _enrollMFA,
child: Text('Start MFA Enrollment'),
),
] else ...[
Text('Scan QR code with your authenticator app:'),
SizedBox(height: 16),
// QR code display (using qr_flutter package)
Container(
width: 200,
height: 200,
color: Colors.grey[300],
child: Center(child: Text('QR Code Display Area')),
),
SizedBox(height: 16),
Text('Secret key: $_secret'),
SizedBox(height: 16),
TextField(
controller: _totpController,
decoration: InputDecoration(
labelText: '6-digit code from authenticator app',
),
keyboardType: TextInputType.number,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _verifyMFA,
child: Text('Verify MFA'),
),
],
],
),
),
);
}
}
User Profile Management
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('Profile updated successfully'),
backgroundColor: Colors.green,
),
);
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Update error: ${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('Profile'),
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('Email: ${user?.email ?? 'N/A'}'),
SizedBox(height: 16),
TextField(
controller: _displayNameController,
decoration: InputDecoration(labelText: 'Display Name'),
),
TextField(
controller: _websiteController,
decoration: InputDecoration(labelText: 'Website'),
),
SizedBox(height: 16),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _updateProfile,
child: Text('Update Profile'),
),
],
),
),
);
}
}
Deep Link Configuration
// 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 Configuration (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>
*/
Custom Local Storage
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);
}
}
// Usage example
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
authOptions: FlutterAuthClientOptions(
localStorage: SecureLocalStorage(),
),
);