Back to Blog
Security
6 min read
January 12, 2024

Secure Password Reset for Flutter Apps: A Complete Guide

Implement secure password reset flows using temporary deep links that work perfectly with your Flutter app's deep linking system. Learn best practices for security, user experience, and implementation.

Overview & Security Benefits

Why Use Deep Links for Password Reset?

Traditional password reset flows often redirect users to web pages, breaking the mobile app experience. With deep linking, users can reset their passwords directly within your Flutter app, maintaining context and providing a seamless experience.

Security Benefits

  • • Time-limited reset tokens (15-30 minutes)
  • • One-time use tokens
  • • Secure token generation
  • • Rate limiting protection
  • • HTTPS enforcement
  • • Audit trail logging

UX Benefits

  • • Native app experience
  • • No context switching
  • • Maintained app state
  • • Consistent UI/UX
  • • Offline capability
  • • Push notification integration

Password Reset Flow

Complete User Journey

1

User Requests Reset

User enters email in "Forgot Password" screen

2

Token Generation

Backend generates secure reset token and creates temporary link

3

Email Delivery

Reset link sent to user's email address

4

Link Click & App Open

User clicks link, app opens to password reset screen

5

Password Update

User enters new password, token is validated and consumed

Backend Implementation

1. Password Reset Request Endpoint

Create an endpoint that handles password reset requests:

javascript
app.post('/api/forgot-password', async (req, res) => {
  try {
    const { email } = req.body;
    
    // 1. Validate email exists
    const user = await findUserByEmail(email);
    if (!user) {
      // Don't reveal if email exists or not
      return res.json({ 
        success: true, 
        message: 'If the email exists, a reset link has been sent.' 
      });
    }
    
    // 2. Generate secure reset token
    const resetToken = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
    
    // 3. Store reset token
    await storeResetToken(user.id, resetToken, expiresAt);
    
    // 4. Create temporary reset link
    const resetLink = await createPasswordResetLink(user.id, resetToken);
    
    // 5. Send reset email
    await sendPasswordResetEmail(email, resetLink);
    
    res.json({ 
      success: true, 
      message: 'If the email exists, a reset link has been sent.' 
    });
    
  } catch (error) {
    console.error('Password reset request error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

2. Reset Link Creation

Create temporary reset links using Redirectly:

javascript
async function createPasswordResetLink(userId, token) {
  try {
    // Create temporary link that expires in 30 minutes
    const response = await redirectly.post('/v1/temp-links', {
      target: `https://yourapp.com/reset-password?token=${token}&user=${userId}`,
      ttlSeconds: 1800 // 30 minutes
    });
    
    return response.data.url;
  } catch (error) {
    console.error('Failed to create reset link:', error);
    throw new Error('Failed to create reset link');
  }
}

async function storeResetToken(userId, token, expiresAt) {
  // Store token in database with expiration
  await db.query(
    'INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES (?, ?, ?, ?)',
    [userId, token, expiresAt, false]
  );
}

3. Password Reset Handler

Handle the actual password reset when the link is clicked:

javascript
app.post('/api/reset-password', async (req, res) => {
  try {
    const { token, userId, newPassword } = req.body;
    
    // 1. Validate token
    const tokenRecord = await db.query(
      'SELECT * FROM password_reset_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW() AND used = false',
      [token, userId]
    );
    
    if (!tokenRecord.length) {
      return res.status(400).json({ error: 'Invalid or expired reset token' });
    }
    
    // 2. Validate new password strength
    if (!isValidPassword(newPassword)) {
      return res.status(400).json({ error: 'Password does not meet requirements' });
    }
    
    // 3. Hash new password
    const hashedPassword = await hashPassword(newPassword);
    
    // 4. Update user password
    await db.query(
      'UPDATE users SET password = ? WHERE id = ?',
      [hashedPassword, userId]
    );
    
    // 5. Mark token as used
    await db.query(
      'UPDATE password_reset_tokens SET used = true WHERE token = ?',
      [token]
    );
    
    // 6. Invalidate all user sessions (optional)
    await invalidateUserSessions(userId);
    
    res.json({
      success: true,
      message: 'Password reset successfully'
    });
    
  } catch (error) {
    console.error('Password reset error:', error);
    res.status(500).json({ error: 'Password reset failed' });
  }
});

Flutter Integration

1. Password Reset Service

Create a service to handle password reset functionality:

dart
class PasswordResetService {
  static final FlutterRedirectly _redirectly = FlutterRedirectly();
  
  static Future<void> initialize() async {
    await _redirectly.initialize(RedirectlyConfig(
      apiKey: 'YOUR_API_KEY',
      baseUrl: 'https://redirectly.app',
      enableDebugLogging: true,
    ));
    
    // Listen for incoming reset links
    _redirectly.onLinkClick.listen(_handlePasswordResetLink);
    
    // Check for initial link
    final initialLink = await _redirectly.getInitialLink();
    if (initialLink != null) {
      _handlePasswordResetLink(initialLink);
    }
  }
  
  static void _handlePasswordResetLink(LinkClickEvent event) {
    if (event.error != null) {
      print('Link error: ${event.error}');
      return;
    }
    
    final uri = Uri.parse(event.originalUrl);
    
    // Check if this is a password reset link
    if (uri.path.contains('/reset-password')) {
      final token = uri.queryParameters['token'];
      final userId = uri.queryParameters['user'];
      
      if (token != null && userId != null) {
        _navigateToPasswordReset(token, userId);
      }
    }
  }
  
  static void _navigateToPasswordReset(String token, String userId) {
    // Navigate to password reset screen with token
    Get.toNamed('/reset-password', arguments: {
      'token': token,
      'userId': userId,
    });
  }
  
  static Future<bool> requestPasswordReset(String email) async {
    try {
      final response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}/api/forgot-password'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({'email': email}),
      );
      
      return response.statusCode == 200;
    } catch (e) {
      print('Password reset request error: $e');
      return false;
    }
  }
  
  static Future<bool> resetPassword(String token, String userId, String newPassword) async {
    try {
      final response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}/api/reset-password'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'token': token,
          'userId': userId,
          'newPassword': newPassword,
        }),
      );
      
      return response.statusCode == 200;
    } catch (e) {
      print('Password reset error: $e');
      return false;
    }
  }
}

2. Password Reset UI

Create the password reset screen:

dart
class PasswordResetScreen extends StatefulWidget {
  final String token;
  final String userId;
  
  const PasswordResetScreen({
    Key? key,
    required this.token,
    required this.userId,
  }) : super(key: key);
  
  @override
  _PasswordResetScreenState createState() => _PasswordResetScreenState();
}

class _PasswordResetScreenState extends State<PasswordResetScreen> {
  final _formKey = GlobalKey<FormState>();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();
  bool _isLoading = false;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Reset Password'),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Padding(
        padding: EdgeInsets.all(24.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Enter your new password',
                style: Theme.of(context).textTheme.headlineSmall,
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 32),
              
              TextFormField(
                controller: _passwordController,
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'New Password',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a password';
                  }
                  if (value.length < 8) {
                    return 'Password must be at least 8 characters';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              
              TextFormField(
                controller: _confirmPasswordController,
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Confirm Password',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value != _passwordController.text) {
                    return 'Passwords do not match';
                  }
                  return null;
                },
              ),
              SizedBox(height: 32),
              
              ElevatedButton(
                onPressed: _isLoading ? null : _resetPassword,
                child: _isLoading 
                  ? CircularProgressIndicator(color: Colors.white)
                  : Text('Reset Password'),
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  Future<void> _resetPassword() async {
    if (!_formKey.currentState!.validate()) return;
    
    setState(() => _isLoading = true);
    
    final success = await PasswordResetService.resetPassword(
      widget.token,
      widget.userId,
      _passwordController.text,
    );
    
    setState(() => _isLoading = false);
    
    if (success) {
      Get.snackbar(
        'Success',
        'Your password has been reset successfully!',
        backgroundColor: Colors.green,
        colorText: Colors.white,
      );
      Get.offAllNamed('/login');
    } else {
      Get.snackbar(
        'Error',
        'Failed to reset password. Please try again.',
        backgroundColor: Colors.red,
        colorText: Colors.white,
      );
    }
  }
}

Security Considerations

Critical Security Measures
  • • Use short expiration times (15-30 minutes)
  • • Implement rate limiting (max 3 requests per hour per email)
  • • Use cryptographically secure random tokens
  • • Never reveal if email exists in system
  • • Invalidate all user sessions after password reset
  • • Log all password reset attempts for monitoring

Rate Limiting

  • • Limit reset requests per email/IP
  • • Implement exponential backoff
  • • Use Redis for distributed rate limiting
  • • Monitor for abuse patterns

Token Security

  • • Generate 32+ byte random tokens
  • • Store tokens with expiration
  • • Mark tokens as used after reset
  • • Clean up expired tokens regularly

Testing & Validation

Testing Checklist

Functional Testing

  • • Test reset request with valid email
  • • Test reset request with invalid email
  • • Verify email delivery
  • • Test link expiration
  • • Test password reset completion
  • • Test token reuse prevention

Security Testing

  • • Test rate limiting
  • • Test with expired tokens
  • • Test with invalid tokens
  • • Test session invalidation
  • • Test password strength validation
  • • Test HTTPS enforcement

🔐 Secure Password Reset Implementation Complete!

You now have a complete, secure password reset system that provides an excellent user experience while maintaining the highest security standards. Your users can reset their passwords seamlessly within your Flutter app.

Related Articles