Send a custom email with a magic link using Firebase

The process to create a magic link is:

terminal
 🚶      [1] Sends email address         🔥
User  ------------------------------> Firebase 
                                    |
                                    | [2] Token generation and 
                                    |     sending magic link back
Email <-----------------------------+
|
| [3] Click on magic link
|                                               🔑
+-----------------------------------> [4] signInWithCustomToken

1. User sends email address

We'll create a component containing:

  1. Input: User will write his email address on this field
  2. Button: Sends email address to Firebase function on click

To get the FIREBASE_MAGIC_LINK_FUNCTION_URL, go to next step.

jsx
import React, { useEffect, useState } from 'react';
import { auth }                       from './Firebase';

const Login = () => {
    
    const [email, setEmail] = useState('');
    
    const sendEmail = async () => {
        
        let url = 'FIREBASE_MAGIC_LINK_FUNCTION_URL';
        
        let response = await fetch(url, {
            
            method: 'POST',
            headers: {'Content-Type':'application/json'},
            body: JSON.stringify({email: email})
            
        });
        
        if(response.ok){
            
            console.log('Magic link sent to the user');
            
        }
        
    }
    
    return(
        <div className = 'MagicLink'>
            <ul>
                <input placeholder = 'Email...' onChange = {e => setEmail(e.target.value)}></input>
                <button onClick = {sendEmail}>Send magic link</button>
            </ul>
        </div>
    );
    
}

export default Login;

2. Token generation and sending magic link back

We need to define a function in Firebase that creates a custom token depending on the email received.

Then, it will generate a magic link using email and token. The URL of the magic link will be such as https://YOUR_DOMAIN.com/v/email=${email}&token=${token}.

Now it's time to configure a custom email template. We'll send the custom email to the user using Nodemailer.

After deploying the function, remember to edit FIREBASE_MAGIC_LINK_FUNCTION_URL in the prior component.

javascript
exports.sendMagicLink = functions.https.onRequest((request, response) => {
    
    const cors = require('cors')({origin: true});
    const nodemailer = require('nodemailer');
    
    return cors(request, response, async () => {
        
        let email = request.body.email;
        let token = await admin.auth().createCustomToken(email);
        
        let magicLink = `https://YOUR_DOMAIN.com/v/email=${email}&token=${token}`;
        
        const mailTransport = nodemailer.createTransport({
            service: 'gmail',
            auth: {
                user: user,
                pass: pass,
            }
        });
        
        const mailOptions = {
            
            from: 'Erik 🤖',
            to: email,
            subject: 'Welcome',
            html: `<p>Hello,</p>
                   <p>Click on this <a href = '${magicLink}'>link</a> to verify your account.</p>
                   <p>Thanks,</p>
                   <p>Erik</p>`
            
        };
        
        await mailTransport.sendMail(mailOptions);
        
        response.sendStatus(200);
        
    });
    
});

3. User clicks on the link

If everything went as expected, the user will receive an email on his inbox.

4. Sign in with custom token

If the user clicks on the received magic link, he'll be redirected to a URL such as https://YOUR_DOMAIN.com/v/email=${email}&token=${token}.

We'll create a component to read the URL parameters and we'll use firebase.auth().signInWithCustomToken() to sign-in and to verify the token as well.

jsx
import React, { useEffect, useState } from 'react';
import firebase, { auth }             from './Firebase';

const Verify = () => {
    
    const [status, setStatus] = useState('processing');
    
    useEffect(() => {
        
        let object = getURLparams();
        
        let email = object.email;
        let token = object.token;
        
        signIn(token);
        
    }, []);
    
    const getURLparams = () => {
        
        let str = window.location.pathname.split('/').pop();
        let arr = str.split('&');
        
        let entries = arr.map(str => [str.split('=')[0], str.split('=')[1]]);
        let object  = Object.fromEntries(entries);
        
        return object;
        
    }
    
    const signIn = async (token) => {
        
        try{
            
            await firebase.auth().signInWithCustomToken(token);
            setStatus('logged');
            
        }
        catch(e){
            
            setStatus('error');
            
        }
        
    }
    
    return(
        <div className = 'Verify'>
            { status === 'processing'
            ? 'Processing'
            : status === 'logged'
            ? 'Logged in'
            : status === 'error'
            ? 'Error'
            : null
            }
        </div>
    );
    
}

export default Verify;

Hi, I'm Erik, an engineer from Barcelona. If you like the post or have any comments, say hi.