flutter_secure_2fa 1.0.2
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.
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'),
),
),
],
),
),
);
}
}