Supabase Auth Dart

Authentication LibrarySupabaseFlutterDartBaaSOAuthJWTMulti-Factor AuthenticationReal-time Auth

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

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(),
  ),
);