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.
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.
User enters email in "Forgot Password" screen
Backend generates secure reset token and creates temporary link
Reset link sent to user's email address
User clicks link, app opens to password reset screen
User enters new password, token is validated and consumed
Create an endpoint that handles password reset requests:
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' }); } });
Create temporary reset links using Redirectly:
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] ); }
Handle the actual password reset when the link is clicked:
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' }); } });
Create a service to handle password reset functionality:
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; } } }
Create the password reset screen:
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, ); } } }
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.