Explore Flask

12.1. Email confirmation

When a new user gives us their email, we generally want to confirm that they gave us the right one. Once we've made that confirmation, we can confidently send password reset links and other sensitive information to our users without wondering who is on the receiving end.

One of the most common patterns for confirming emails is to send a password reset link with a unique URL that, when visited, confirms that user's email address. For example, john@gmail.com signs up at our application. We register him in the database with an email_confirmed column set to False and fire off an email to john@gmail.com with a unique URL. This URL usually contains a unique token, e.g. http://myapp.com/accounts/confirm-/Q2hhZCBDYXRsZXR0IHJvY2tzIG15IHNvY2tz. When John gets that email, he clicks the link. Our app sees the token, knows which email to confirm and sets John's email_confirmed column to True.

How do we know which email to confirm with a given token? One way would be to store the token in the database when it is created and check that table when we receive the confirmation request. That's a lot of overhead and, lucky for us, it's unnecessary.

We're going to encode the email address in the token. The token will also contain a timestamp to let us set a time limit on how long it's valid. To do this, we'll use the itsdangerous package. This package gives us tools to send sensitive data into untrusted environments (like sending an email confirmation token to an unconfirmed email). In this case, we're going to use an instance of the URLSafeTimedSerializer class.

# ourapp/util/security.py

from itsdangerous import URLSafeTimedSerializer

from .. import app

ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])

We can use that serializer to generate a confirmation token when a user gives us their email address. We'll implement a simple account creation process using this method.

# ourapp/views.py

from flask import redirect, render_template, url_for

from . import app, db
from .forms import EmailPasswordForm
from .util import ts, send_email

@app.route('/accounts/create', methods=["GET", "POST"])
def create_account():
    form = EmailPasswordForm()
    if form.validate_on_submit():
        user = User(
            email = form.email.data,
            password = form.password.data
        )
        db.session.add(user)
        db.session.commit()

        # Now we'll send the email confirmation link
        subject = "Confirm your email"

        token = ts.dumps(self.email, salt='email-confirm-key')

        confirm_url = url_for(
            'confirm_email',
            token=token,
            _external=True)

        html = render_template(
            'email/activate.html',
            confirm_url=confirm_url)

        # We'll assume that send_email has been defined in myapp/util.py
        send_email(user.email, subject, html)

        return redirect(url_for("index"))

    return render_template("accounts/create.html", form=form)

The view that we've defined handles the creation of the user and sends off an email to the given email address. You may notice that we're using a template to generate the HTML for the email.

{# ourapp/templates/email/activate.html #}

Your account was successfully created. Please click the link below<br>
to confirm your email address and activate your account:

<p>
<a href="{{ confirm_url }}">{{ confirm_url }}</a>
</p>

<p>
--<br>
Questions? Comments? Email [email protected].
</p>

Okay, so now we just need to implement a view that handles the confirmation link in that email.

# ourapp/views.py

@app.route('/confirm/<token>')
def confirm_email(token):
    try:
        email = ts.loads(token, salt="email-confirm-key", max_age=86400)
    except:
        abort(404)

    user = User.query.filter_by(email=email).first_or_404()

    user.email_confirmed = True

    db.session.add(user)
    db.session.commit()

    return redirect(url_for('signin'))

This view is a simple form view. We just add the try ... except bit at the beginning to check that the token is valid. The token contains a timestamp, so we can tell ts.loads() to raise an exception if it is older than max_age. In this case, we're setting max_age to 86400 seconds, i.e. 24 hours.

Note You can use very similar methods to implement an email update feature. Just send a confirmation link to the new email address with a token that contains both the old and the new addresses. If the token is valid, update the old address with the new one.