Back to Blog
Authentication
8 min read
January 15, 2024

Email Verification with Deep Linking: Complete Implementation Guide

Learn how to implement secure email verification using temporary deep links that work seamlessly across web and mobile platforms. This comprehensive guide covers everything from backend setup to Flutter integration.

Overview & Benefits

What is Email Verification with Deep Linking?

Email verification with deep linking allows users to verify their email addresses by clicking a link that opens directly in your app, providing a seamless experience across web and mobile platforms. Instead of traditional web-only verification, users get a native app experience with proper navigation and state management.

Key Benefits

  • • Seamless cross-platform experience
  • • Higher verification completion rates
  • • Better user engagement and retention
  • • Secure token-based verification
  • • Automatic expiration for security
  • • Detailed analytics and tracking

Use Cases

  • • User account registration
  • • Email address changes
  • • Two-factor authentication setup
  • • Newsletter subscriptions
  • • Account recovery processes
  • • Marketing campaign opt-ins

System Architecture

How It Works

1

User Registration

User signs up with email address in your Flutter app

2

Verification Link Creation

Backend creates temporary verification link using Redirectly API

3

Email Delivery

Verification link sent to user's email address

4

Link Click & Verification

User clicks link, app opens, email gets verified

Backend Implementation

1. User Registration Endpoint

First, create an endpoint that handles user registration and triggers email verification:

javascript
// Node.js/Express example
const axios = require('axios');
const crypto = require('crypto');

// Initialize Redirectly client
const redirectly = axios.create({
  baseURL: 'https://redirectly.app/api',
  headers: {
    'Authorization': `Bearer ${process.env.REDIRECTLY_API_KEY}`,
    'Content-Type': 'application/json'
  }
});

app.post('/api/register', async (req, res) => {
  try {
    const { email, password, username } = req.body;
    
    // 1. Create user account (unverified)
    const user = await createUser({
      email,
      password: await hashPassword(password),
      username,
      email_verified: false
    });
    
    // 2. Generate verification token
    const verificationToken = crypto.randomBytes(32).toString('hex');
    await storeVerificationToken(user.id, verificationToken);
    
    // 3. Create temporary verification link
    const verificationLink = await createVerificationLink(user.id, verificationToken);
    
    // 4. Send verification email
    await sendVerificationEmail(email, verificationLink);
    
    res.json({
      success: true,
      message: 'Registration successful. Please check your email to verify your account.',
      user: { id: user.id, email, username }
    });
    
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({ error: 'Registration failed' });
  }
});

2. Verification Link Creation

Create a function that generates temporary verification links using Redirectly:

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

async function storeVerificationToken(userId, token) {
  // Store token in database with expiration
  await db.query(
    'INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
    [userId, token, new Date(Date.now() + 24 * 60 * 60 * 1000)]
  );
}

3. Email Verification Handler

Create an endpoint that handles the verification when the link is clicked:

javascript
app.get('/api/verify-email', async (req, res) => {
  try {
    const { token, user } = req.query;
    
    // 1. Validate token
    const tokenRecord = await db.query(
      'SELECT * FROM email_verification_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW()',
      [token, user]
    );
    
    if (!tokenRecord.length) {
      return res.status(400).json({ error: 'Invalid or expired verification token' });
    }
    
    // 2. Mark email as verified
    await db.query(
      'UPDATE users SET email_verified = true WHERE id = ?',
      [user]
    );
    
    // 3. Delete used token
    await db.query(
      'DELETE FROM email_verification_tokens WHERE token = ?',
      [token]
    );
    
    // 4. Return success response
    res.json({
      success: true,
      message: 'Email verified successfully'
    });
    
  } catch (error) {
    console.error('Email verification error:', error);
    res.status(500).json({ error: 'Verification failed' });
  }
});

Flutter Integration

1. Setup Deep Link Handling

Configure your Flutter app to handle incoming verification links:

dart
import 'package:flutter_redirectly/flutter_redirectly.dart';

class EmailVerificationService {
  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 links
    _redirectly.onLinkClick.listen(_handleIncomingLink);
    
    // Check for initial link (app opened from link)
    final initialLink = await _redirectly.getInitialLink();
    if (initialLink != null) {
      _handleIncomingLink(initialLink);
    }
  }
  
  static void _handleIncomingLink(LinkClickEvent event) {
    if (event.error != null) {
      print('Link error: ${event.error}');
      return;
    }
    
    final uri = Uri.parse(event.originalUrl);
    
    // Check if this is an email verification link
    if (uri.path.contains('/verify-email')) {
      final token = uri.queryParameters['token'];
      final userId = uri.queryParameters['user'];
      
      if (token != null && userId != null) {
        _processEmailVerification(token, userId);
      }
    }
  }
  
  static Future<void> _processEmailVerification(String token, String userId) async {
    try {
      // Call your backend to verify the email
      final response = await http.get(
        Uri.parse('${ApiConfig.baseUrl}/api/verify-email?token=$token&user=$userId'),
        headers: {'Content-Type': 'application/json'},
      );
      
      if (response.statusCode == 200) {
        // Show success message
        _showVerificationSuccess();
        
        // Update app state
        await AuthService.updateEmailVerificationStatus(true);
        
        // Navigate to main app
        Get.offAllNamed('/home');
      } else {
        _showVerificationError('Verification failed. Please try again.');
      }
    } catch (e) {
      _showVerificationError('Network error. Please check your connection.');
    }
  }
  
  static void _showVerificationSuccess() {
    Get.snackbar(
      'Success',
      'Your email has been verified successfully!',
      backgroundColor: Colors.green,
      colorText: Colors.white,
      duration: Duration(seconds: 3),
    );
  }
  
  static void _showVerificationError(String message) {
    Get.snackbar(
      'Error',
      message,
      backgroundColor: Colors.red,
      colorText: Colors.white,
      duration: Duration(seconds: 5),
    );
  }
}

2. Initialize in main.dart

Initialize the email verification service when your app starts:

dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize email verification service
  await EmailVerificationService.initialize();
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AuthService.isLoggedIn() 
        ? HomeScreen() 
        : LoginScreen(),
      routes: {
        '/home': (context) => HomeScreen(),
        '/login': (context) => LoginScreen(),
        '/verify-email': (context) => EmailVerificationScreen(),
      },
    );
  }
}

Security Best Practices

Critical Security Measures
  • • Use cryptographically secure random tokens (32+ bytes)
  • • Set appropriate expiration times (24 hours max for email verification)
  • • Implement rate limiting on verification endpoints
  • • Use HTTPS for all verification links
  • • Validate tokens server-side before processing
  • • Delete used tokens immediately after verification

Token Security

  • • Generate tokens using crypto.randomBytes()
  • • Store tokens with expiration timestamps
  • • Use one-time tokens (delete after use)
  • • Implement token rotation for resend requests

Rate Limiting

  • • Limit verification attempts per user
  • • Implement exponential backoff
  • • Monitor for suspicious activity
  • • Block IPs with excessive attempts

Testing & Validation

Testing Checklist

Functional Testing

  • • Test link generation and expiration
  • • Verify email delivery
  • • Test app opening from email
  • • Validate verification completion
  • • Test expired link handling
  • • Verify error handling

Security Testing

  • • Test with invalid tokens
  • • Verify rate limiting works
  • • Test token reuse prevention
  • • Validate HTTPS enforcement
  • • Test with expired tokens
  • • Verify user isolation

Sample Test Cases

javascript
// Jest test example
describe('Email Verification', () => {
  test('should create verification link successfully', async () => {
    const mockUser = { id: '123', email: 'test@example.com' };
    const mockToken = 'valid-token-123';
    
    const link = await createVerificationLink(mockUser.id, mockToken);
    
    expect(link).toContain('redirectly.app');
    expect(link).toContain(mockToken);
  });
  
  test('should reject expired tokens', async () => {
    const expiredToken = 'expired-token';
    const userId = '123';
    
    const response = await request(app)
      .get(`/api/verify-email?token=${expiredToken}&user=${userId}`);
    
    expect(response.status).toBe(400);
    expect(response.body.error).toContain('expired');
  });
  
  test('should prevent token reuse', async () => {
    const token = 'used-token';
    const userId = '123';
    
    // First verification should succeed
    const firstResponse = await request(app)
      .get(`/api/verify-email?token=${token}&user=${userId}`);
    expect(firstResponse.status).toBe(200);
    
    // Second verification should fail
    const secondResponse = await request(app)
      .get(`/api/verify-email?token=${token}&user=${userId}`);
    expect(secondResponse.status).toBe(400);
  });
});

🎉 You're Ready to Implement Email Verification!

You now have everything you need to implement secure email verification with deep linking in your Flutter app. This approach provides a seamless user experience while maintaining high security standards.

Related Articles