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.