flutter_secure_2fa 1.0.2 copy "flutter_secure_2fa: ^1.0.2" to clipboard
flutter_secure_2fa: ^1.0.2 copied to clipboard

A secure and flexible 2FA package for Flutter using TOTP. Supports SHA1/SHA256/SHA512 algorithms, variable digits, and time window verification.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_2fa/flutter_secure_2fa.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:pinput/pinput.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Secure 2FA Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Flutter Secure 2FA Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Secure your app with 2FA',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 30),
            ElevatedButton.icon(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const ActivationScreen(),
                  ),
                );
              },
              icon: const Icon(Icons.qr_code),
              label: const Text('Setup 2FA (Generate QR)'),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(
                  horizontal: 24,
                  vertical: 12,
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton.icon(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const VerifyScreen()),
                );
              },
              icon: const Icon(Icons.lock_open),
              label: const Text('Verify 2FA Code'),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(
                  horizontal: 24,
                  vertical: 12,
                ),
              ),
            ),
            const SizedBox(height: 20),
            OutlinedButton(
              onPressed: () {
                // Example of validating a hardcoded advanced case
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const VerifyScreen(isAdvanced: true),
                  ),
                );
              },
              child: const Text("Advanced Verification (SHA256 / 8 Digits)"),
            ),
          ],
        ),
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// Screen 1: Activation (Generate Secret & QR Code)
// -----------------------------------------------------------------------------
class ActivationScreen extends StatefulWidget {
  const ActivationScreen({super.key});

  @override
  State<ActivationScreen> createState() => _ActivationScreenState();
}

class _ActivationScreenState extends State<ActivationScreen> {
  final FlutterSecure2FA _secure2FA = FlutterSecure2FA();
  final String _email = 'user@example.com';
  String? _secret;
  String? _authUrl;

  @override
  void initState() {
    super.initState();
    _generateSecret();
  }

  void _generateSecret() {
    // Generate a new random Base32 secret
    final secret = _secure2FA.generateSecret();

    // Get the otpauth URL for the QR code
    // You can customize appName and accountName
    final authUrl = _secure2FA.getAuthUrl(
      secret,
      appName: 'Flutter Secure 2FA Example',
      accountName: _email,
    );

    setState(() {
      _secret = secret;
      _authUrl = authUrl;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Scan QR Code')),
      body: Center(
        child: _secret == null
            ? const CircularProgressIndicator()
            : SingleChildScrollView(
                padding: const EdgeInsets.all(24.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const Text(
                      'Scan this with Google Authenticator',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 20),
                    // Using qr_flutter to render the QR code
                    QrImageView(
                      data: _authUrl!,
                      version: QrVersions.auto,
                      size: 220.0,
                    ),
                    const SizedBox(height: 20),
                    const Text(
                      'Or enter this key manually:',
                      style: TextStyle(color: Colors.grey),
                    ),
                    const SizedBox(height: 8),
                    SelectableText(
                      _secret!,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        letterSpacing: 1.5,
                      ),
                    ),
                    IconButton(
                      icon: const Icon(Icons.copy),
                      onPressed: () {
                        Clipboard.setData(ClipboardData(text: _secret!));
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text('Secret copied!')),
                        );
                      },
                    ),
                    const SizedBox(height: 40),
                    ElevatedButton(
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => VerifyScreen(
                              secret:
                                  _secret, // Pass secret to verify immediately
                              isActivating: true,
                            ),
                          ),
                        );
                      },
                      child: const Text('I have scanned it, Continue'),
                    ),
                  ],
                ),
              ),
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// Screen 2: Verification (Enter Code)
// -----------------------------------------------------------------------------
class VerifyScreen extends StatefulWidget {
  final String? secret;
  final bool isActivating;
  final bool isAdvanced;

  const VerifyScreen({
    super.key,
    this.secret,
    this.isActivating = false,
    this.isAdvanced = false,
  });

  @override
  State<VerifyScreen> createState() => _VerifyScreenState();
}

class _VerifyScreenState extends State<VerifyScreen> {
  final FlutterSecure2FA _secure2FA = FlutterSecure2FA();
  final TextEditingController _pinController = TextEditingController();

  String? _currentSecret;
  bool _isLoading = false;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _loadSecret();
  }

  Future<void> _loadSecret() async {
    if (widget.isActivating) {
      _currentSecret = widget.secret;
    } else {
      // In a real app, retrieve the secret from secure storage
      final prefs = await SharedPreferences.getInstance();
      _currentSecret = prefs.getString('2fa_secret');

      if (_currentSecret == null && !widget.isAdvanced) {
        setState(
          () =>
              _errorMessage = "2FA not set up. Please go back and setup first.",
        );
      } else if (widget.isAdvanced) {
        // For demo advanced mode, we generate a temp secret if not strictly checking logic
        // But to make it work, we'd need a separate setup.
        // For this example, let's just use a dummy valid one or reuse the same.
        // Or generate one on the fly to test logic (but user can't verify it easily without adding to app).
        // Let's just use the stored one but verify with different params to show API.
        _currentSecret = prefs.getString('2fa_secret');
      }
    }
  }

  Future<void> _verify() async {
    if (_currentSecret == null) return;

    final code = _pinController.text;
    final requiredDigits = widget.isAdvanced ? 8 : 6;

    if (code.length != requiredDigits) {
      setState(() => _errorMessage = "Enter a $requiredDigits-digit code");
      return;
    }

    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    bool isValid;
    if (widget.isAdvanced) {
      // ADVANCED USAGE EXAMPLE
      // Verifying with SHA256, 8 digits, and wider clock drift window
      // Note: The secret must have been generated and added to authenticator
      // with these same settings for this to actually pass TRUE.
      // This is primarily to demonstrate the API capability.
      isValid = _secure2FA.verifyCode(
        _currentSecret!,
        code,
        algorithm: Secure2FAAlgorithm.sha256,
        digits: 8,
        interval: 30,
        window: 2, // Check +/- 2 intervals (1 minute drift tolerance)
      );
    } else {
      // STANDARD USAGE
      // Defaults: SHA1, 6 digits, 30s interval, window 1
      isValid = _secure2FA.verifyCode(_currentSecret!, code);
    }

    setState(() => _isLoading = false);

    if (isValid) {
      if (widget.isActivating) {
        // Save the secret
        final prefs = await SharedPreferences.getInstance();
        await prefs.setString('2fa_secret', _currentSecret!);

        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('2FA Activated Successfully!')),
          );
          Navigator.popUntil(context, (route) => route.isFirst);
        }
      } else {
        if (mounted) {
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              title: const Text("Success"),
              content: const Text("Code Validated! User Logged In."),
              actions: [
                TextButton(
                  onPressed: () {
                    Navigator.pop(context); // Close dialog
                    Navigator.pop(context); // Go back home
                  },
                  child: const Text("OK"),
                ),
              ],
            ),
          );
        }
      }
    } else {
      setState(() => _errorMessage = "Invalid Code. Please try again.");
    }
  }

  @override
  Widget build(BuildContext context) {
    final digitCount = widget.isAdvanced ? 8 : 6;

    return Scaffold(
      appBar: AppBar(
        title: Text(
          widget.isActivating ? 'Verify Activation' : 'Login with 2FA',
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              widget.isActivating
                  ? 'Enter the code from Authenticator to confirm.'
                  : (widget.isAdvanced
                        ? 'Enter 8-digit SHA256 Code'
                        : 'Enter 6-digit Login Code'),
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 16),
            ),
            const SizedBox(height: 30),
            Pinput(
              controller: _pinController,
              length: digitCount,
              onCompleted: (_) => _verify(),
              defaultPinTheme: PinTheme(
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.blueAccent),
                  borderRadius: BorderRadius.circular(10),
                ),
                textStyle: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            if (_errorMessage != null) ...[
              const SizedBox(height: 20),
              Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
            ],
            const SizedBox(height: 30),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _isLoading ? null : _verify,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: _isLoading
                    ? const SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('Verify'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
2
likes
150
points
124
downloads

Publisher

unverified uploader

Weekly Downloads

A secure and flexible 2FA package for Flutter using TOTP. Supports SHA1/SHA256/SHA512 algorithms, variable digits, and time window verification.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

base32, flutter, otp

More

Packages that depend on flutter_secure_2fa