In this guide, you’ll learn the ins and outs of checking registrants’ email addresses for accuracy.
Support for Python 3 has been added as of 04/30/2015.
When a user creates a new account, the workflow begins with the sending of a confirmation email. Until the user “confirms” their account by following the email’s instructions, the account will remain in a “unconfirmed” status. Most online apps adhere to this straightforward procedure.
What kind of actions can anonymous individuals perform is a crucial consideration. Do they have complete access, certain restrictions, or no access at all to your application? Users who have not yet confirmed their accounts can log in to the application used in this lesson, but they will be taken to a confirmation screen before they can use the programme proper.
Before we get started, it’s worth noting that the majority of what we’ll be implementing is already available as part of the Flask-User and Flask-Security extensions. First and foremost, though, you should take this as a learning opportunity. Additionally, there are constraints on both of these add-ons, such as the databases they can interact with. What if, for instance, you wished to employ RethinkDB?
Let’s
begin.
Flask basic registration
To get started, we’ll use a premade Flask template for registering new users. Simply get the source code from the repository. After a virtualenv has been set up and made active, you may use the following instructions to get up and running in no time.
started:
$ pip install -r requirements.txt
$ export APP_SETTINGS="project.config.DevelopmentConfig"
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
$ python manage.py runserver
For more details, please refer to the readme file.
Launch the application and then go to http://localhost:5000/register to create a new account. After signing up, the programme will instantly log you in and take you to the home screen. Explore the environment and the code, especially the “user” template.
Stop the machine when
done.
Update the current app
Models
Let’s begin by updating our User model in project/models.py to include a confirmed field.
:
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
registered_on = db.Column(db.DateTime, nullable=False)
admin = db.Column(db.Boolean, nullable=False, default=False)
confirmed = db.Column(db.Boolean, nullable=False, default=False)
confirmed_on = db.Column(db.DateTime, nullable=True)
def __init__(self, email, password, confirmed,
paid=False, admin=False, confirmed_on=None):
self.email = email
self.password = bcrypt.generate_password_hash(password)
self.registered_on = datetime.datetime.now()
self.admin = admin
self.confirmed = confirmed
self.confirmed_on = confirmed_on
Take note that the default value for this field is false. An additional datetime-type field, confirmed_on (https://realpython.com/python-datetime/), has been introduced. The difference between the registered_on and confirmed_on dates can be analysed using cohort analysis, thus I like to include this information as well.
Let’s delete everything we’ve migrated and start from scratch with our database. So, get rid of the “migrations” and the dev.sqlite database.
folder.
Manage command
The create_admin command in manage.py must then be modified to account for the newly added fields in the database.
account:
@manager.command
def create_admin():
"""Creates the admin user."""
db.session.add(User(
email="ad@min.com",
password="admin",
admin=True,
confirmed=True,
confirmed_on=datetime.datetime.now())
)
db.session.commit()
Be sure to bring in datetime. Execute the following instructions now:
again:
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
The last step before resuming user registration is to update the register() view method in project/user/views.py.
…
Change:
user = User(
email=form.email.data,
password=form.password.data
)
To:
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
Does that make sense? Let’s consider the possible motivations for setting the confirmed default to false.
Okay. Resume using the programme. To create a new account, go to http://localhost:5000/register. Your SQLite database should look like this when viewed in the SQLite Browser:
So, the new user I added, michael@realpython.com, is still pending verification. Please modify
that.
Add email confirmation
Generate confirmation token
To verify an account, a user should just click the confirmation URL provided in the email. The perfect URL would read as follows: http://yourapp.com/confirm/id>. In this case, the id is crucial. Using the itsdangerous package, we will include the user’s email address (along with a timestamp) in the id.
Put the following into a file named project/token.py:
code:
# project/token.py
from itsdangerous import URLSafeTimedSerializer
from project import app
def generate_confirmation_token(email):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
def confirm_token(token, expiration=3600):
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
try:
email = serializer.loads(
token,
salt=app.config['SECURITY_PASSWORD_SALT'],
max_age=expiration
)
except:
return False
return email
The user’s registered email address is used along with the URLSafeTimedSerializer in the generate_confirmation_token() function to provide a unique token. The token contains the actual email address. The loads() method in the confirm_token() function accepts the token and its expiration time (in this case, one hour or 3,600 seconds) as parameters, allowing us to successfully validate the token. If the token has not yet expired, a response will be sent.
Make that the SECURITY_PASSWORD_SALT variable is set in the BaseConfig of your application.()
):
SECURITY_PASSWORD_SALT = 'my_precious_two'
Update
Now, under project/user/views.py, we will re-update the register() view function.
:
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
db.session.add(user)
db.session.commit()
token = generate_confirmation_token(user.email)
Don’t forget to refresh the
imports:
from project.token import generate_confirmation_token, confirm_token
Handle Email Confirmation
Let’s create a new view to deal with the email in the next step.
confirmation:
@user_blueprint.route('/confirm/<token>')
@login_required
def confirm_email(token):
try:
email = confirm_token(token)
except:
flash('The confirmation link is invalid or has expired.', 'danger')
user = User.query.filter_by(email=email).first_or_404()
if user.confirmed:
flash('Account already confirmed. Please login.', 'success')
else:
user.confirmed = True
user.confirmed_on = datetime.datetime.now()
db.session.add(user)
db.session.commit()
flash('You have confirmed your account. Thanks!', 'success')
return redirect(url_for('main.home'))
You may find views.py in the project’s user directory. Also, remember to refresh the
imports:
import datetime
Here, we send the token to the confirm_token() function. If the confirmation is successful, the user is updated by updating the email_confirmed property to True and the time the confirmation occurred to the current time. We also notify the user of this fact if they have already gone through the confirmation process and been confirmed.
this.
Create the email template
An initial email will be added next.
template:
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
<br>
<p>Cheers!</p>
Put this in “project/templates/user” as “activate.html.” In the register() method, a single variable named confirm_url is required.
function.
Send email
Let’s use Flask-Mail, which we’ve already installed and set up in project/__init__.py, to build an easy-to-use email sending function.
Make sure you have a file named email.py.
:
# project/email.py
from flask.ext.mail import Message
from project import app, mail
def send_email(to, subject, template):
msg = Message(
subject,
recipients=[to],
html=template,
sender=app.config['MAIL_DEFAULT_SENDER']
)
mail.send(msg)
Do so in the “project” directory.
Thus, we need only supply an address list, a topic, and a draught template. The email setup settings are outside the scope of this article.
bit.
Update
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(
email=form.email.data,
password=form.password.data,
confirmed=False
)
db.session.add(user)
db.session.commit()
token = generate_confirmation_token(user.email)
confirm_url = url_for('user.confirm_email', token=token, _external=True)
html = render_template('user/activate.html', confirm_url=confirm_url)
subject = "Please confirm your email"
send_email(user.email, subject, html)
login_user(user)
flash('A confirmation email has been sent via email.', 'success')
return redirect(url_for("main.home"))
return render_template('user/register.html', form=form)
Please import the following as
well:
from project.email import send_email
We are currently assembling the whole thing. This feature regulates the process as a whole, either immediately or indirectly:
- Handle initial registration,
- Generate token and confirmation URL,
- Send confirmation email,
- Flash confirmation,
- Log in the user, and
- Redirect user.
Did you take note of the _external=True option? This appends the complete absolute URL, which in our case is http://localhost:5000.
Setting up our email is a prerequisite to testing this.
settings.
Mail
To begin, edit project/config.py and update the BaseConfig() function.
:
class BaseConfig(object):
"""Base configuration."""
# main config
SECRET_KEY = 'my_precious'
SECURITY_PASSWORD_SALT = 'my_precious_two'
DEBUG = False
BCRYPT_LOG_ROUNDS = 13
WTF_CSRF_ENABLED = True
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
# mail settings
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
# gmail authentication
MAIL_USERNAME = os.environ['APP_MAIL_USERNAME']
MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
# mail accounts
MAIL_DEFAULT_SENDER = 'from@example.com'
Look at the
check out the Flask-Mail documentation for the whole scoop.
You can use your existing GMAIL account, or create a new one as a test. Then, in the current shell, temporarily alter the environment variables.
session:
$ export APP_MAIL_USERNAME="foo"
$ export APP_MAIL_PASSWORD="bar"
For those whose Gmail accounts have
Google will prevent access with their 2FA system.
Right, let’s
test!
First test
To register for the app, use your browser and go to http://localhost:5000/. Then sign up using an active email address. You should have received an email similar to the following if everything went smoothly:
If you follow the link, you’ll arrive at http://localhost:5000/. Validate that the user exists in the database, that the confirmed field contains True, and that the confirmed_on field has a timestamp.
field.
Nice!
Handle permissions
We agreed at the outset of this tutorial that “unconfirmed users can log in but they should be immediately redirected to a page – let’s call the route /unconfirmed – reminding users that they need to confirm their account before they can access the application.”
This means that
to-
-
Add the
/unconfirmed
route -
Add an
unconfirmed.html
template -
Update the
register()
view function - Create a decorator
-
Update
navigation.html
template
Add
To project/user/views.py, append the following route:
:
@user_blueprint.route('/unconfirmed')
@login_required
def unconfirmed():
if current_user.confirmed:
return redirect('main.home')
flash('Please confirm your account!', 'warning')
return render_template('user/unconfirmed.html')
You’re familiar with this sort of coding, so shall we proceed?
on.
Add
{% extends "_base.html" %}
{% block content %}
<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="/">Resend</a>.</p>
{% endblock %}
Unconfirmed.html should be saved in the “project/templates/user” directory. Once more, this should be a simple process. We’ve implemented a fake URL to resend the confirmation email for the time being. We’ll talk more about this later.
down.
Update the
First, let’s
change:
return redirect(url_for("main.home"))
To:
return redirect(url_for("user.unconfirmed"))
The user will now be taken directly to the /unconfirmed page when the confirmation email has been issued.
route.
Create a decorator
# project/decorators.py
from functools import wraps
from flask import flash, redirect, url_for
from flask.ext.login import current_user
def check_confirmed(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if current_user.confirmed is False:
flash('Please confirm your account!', 'warning')
return redirect(url_for('user.unconfirmed'))
return func(*args, **kwargs)
return decorated_function
A simple method of determining whether or not a user has been validated is provided here. The user is sent to the /unconfirmed path if the confirmation status is false. Put this in the “project” folder as decorators.py.
It’s time to spruce up the profile() view.
function:
@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
# ... snip ...
Remember to bring in the
decorator:
from project.decorators import check_confirmed
Update
Finally, please refresh this section of the navigation.html
template-
Change:
<ul class="nav navbar-nav">
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for('user.profile') }}">Profile</a></li>
{% endif %}
</ul>
To:
<ul class="nav navbar-nav">
{% if current_user.confirmed and current_user.is_authenticated() %}
<li><a href="{{ url_for('user.profile') }}">Profile</a></li>
{% elif current_user.is_authenticated() %}
<li><a href="{{ url_for('user.unconfirmed') }}">Confirm</a></li>
{% endif %}
</ul>
It’s trial time.
again!
Second test
Start up the app and sign up once more, this time using an active email account. (Feel free to delete the old user that you registered before first from the database to use again.) After signing up, you should be taken to http://localhost:5000/unconfirmed.
Check that your http://localhost:5000/profile link works properly. Follow the link to http://localhost:5000/unconfirmed whether this works as intended.
Just verify your email and you’ll have complete access to the site.
Boom!
Resend email
Let’s get the resend button operational at last. Please modify project/user/views.py to include the following view function.
:
@user_blueprint.route('/resend')
@login_required
def resend_confirmation():
token = generate_confirmation_token(current_user.email)
confirm_url = url_for('user.confirm_email', token=token, _external=True)
html = render_template('user/activate.html', confirm_url=confirm_url)
subject = "Please confirm your email"
send_email(current_user.email, subject, html)
flash('A new confirmation email has been sent.', 'success')
return redirect(url_for('user.unconfirmed'))
It is time to update the unverified.html.
template:
{% extends "_base.html" %}
{% block content %}
<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="{{ url_for('user.resend_confirmation') }}">Resend</a>.</p>
{% endblock %}
Third test
So, you know the deal. Please resend a confirmation email and check the link before proceeding. The plan should be effective.
In conclusion, what occurs if you use a few confirmation links to reply to yourself? Do they all hold water? Try it and see. Create a new account and send few confirmation emails. First email confirmation is preferable. How successful was it? It ought to. How about this? When a new email is sent, do you think the old ones should be deleted?
Please investigate this further. Try it out with some of your favourite web apps, too! The way they deal with it
behaviour?
Update test suite
Alright. That wraps up the critical features, then. Given that the present test suite is, in fact, flawed, I propose that we upgrade it. Test it out
:
$ python manage.py test
This is what you need to see
error:
TypeError: __init__() takes at least 4 arguments (3 given)
The fix is a simple modification to the setUp() function in the project/util.py file.
:
def setUp(self):
db.create_all()
user = User(email="ad@min.com", password="admin_user", confirmed=False)
db.session.add(user)
db.session.commit()
It is time to rerun the tests. Success is expected!