This tutorial details how to validate email addresses during user registration.

Updated 04/30/2015: Added Python 3 support.


In terms of workflow, after a user registers a new account, a confirmation email is sent. The user account is marked as "unconfirmed" until the user, well, "confirms" the business relationship via the instructions in the email. This is a unproblematic workflow that most web applications follow.

One important affair to have into account is what unconfirmed users are allowed to practice. In other words, do they accept total access to your awarding, limited/restricted access, or no access at all? For the awarding in this tutorial, unconfirmed users can log in merely they are immediately redirected to a page reminding them that they need to ostend their account before they can access the application.

Before beginning, most of the functionality that we will be adding is function of the Flask-User and Flask-Security extensions - which begs the question why not just apply the extensions? Well, first and foremost, this is an opportunity to learn. Also, both of those extensions have limitations, like the supported databases. What if you wanted to use RethinkDB, for example?

Allow's begin.

Flask basic registration

We're going to outset with a Flask boilerplate that includes basic user registration. Grab the code from the repository. In one case you've created and activated a virtualenv, run the post-obit commands to quickly get started:

                                            $                pip install -r requirements.txt                $                                consign                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                          

Check out the readme for more information.

With the app running, navigate to http://localhost:5000/annals and annals a new user. Notice that afterward registration, the app automatically logs you in and redirects you lot to the main page. Take a look effectually, then run through the code - specifically the "user" blueprint.

Impale the server when done.

Update the electric current app

Models

First, let'south add the confirmed field to our User model in project/models.py:

                                                  class                  User                  (                  db                  .                  Model                  ):                  __tablename__                  =                  "users"                  id                  =                  db                  .                  Column                  (                  db                  .                  Integer                  ,                  primary_key                  =                  True                  )                  email                  =                  db                  .                  Cavalcade                  (                  db                  .                  String                  ,                  unique                  =                  True                  ,                  nullable                  =                  Fake                  )                  password                  =                  db                  .                  Cavalcade                  (                  db                  .                  String                  ,                  nullable                  =                  Simulated                  )                  registered_on                  =                  db                  .                  Cavalcade                  (                  db                  .                  DateTime                  ,                  nullable                  =                  False                  )                  admin                  =                  db                  .                  Cavalcade                  (                  db                  .                  Boolean                  ,                  nullable                  =                  False                  ,                  default                  =                  False                  )                  confirmed                  =                  db                  .                  Column                  (                  db                  .                  Boolean                  ,                  nullable                  =                  False                  ,                  default                  =                  Faux                  )                  confirmed_on                  =                  db                  .                  Column                  (                  db                  .                  DateTime                  ,                  nullable                  =                  True                  )                  def                  __init__                  (                  self                  ,                  email                  ,                  password                  ,                  confirmed                  ,                  paid                  =                  False                  ,                  admin                  =                  Simulated                  ,                  confirmed_on                  =                  None                  ):                  cocky                  .                  email                  =                  email                  self                  .                  countersign                  =                  bcrypt                  .                  generate_password_hash                  (                  password                  )                  cocky                  .                  registered_on                  =                  datetime                  .                  datetime                  .                  now                  ()                  cocky                  .                  admin                  =                  admin                  cocky                  .                  confirmed                  =                  confirmed                  cocky                  .                  confirmed_on                  =                  confirmed_on                              

Notice how this field defaults to 'False'. Nosotros also added a confirmed_on field, which is a [datetime] (https://realpython.com/python-datetime/). I like to include this field as well in order to analyze the difference between the registered_on and confirmed_on dates using cohort assay.

Allow's completely start over with our database and migrations. So, go ahead and delete the database, dev.sqlite, as well as the "migrations" folder.

Manage control

Adjacent, within manage.py, update the create_admin command to accept the new database fields into business relationship:

                                                  @manager                  .                  command                  def                  create_admin                  ():                  """Creates the admin user."""                  db                  .                  session                  .                  add                  (                  User                  (                  email                  =                  "advertisement@min.com"                  ,                  password                  =                  "admin"                  ,                  admin                  =                  Truthful                  ,                  confirmed                  =                  True                  ,                  confirmed_on                  =                  datetime                  .                  datetime                  .                  now                  ())                  )                  db                  .                  session                  .                  commit                  ()                              

Brand certain to import datetime. Now, go alee and run the following commands again:

                                                  $                  python manage.py create_db                  $                  python manage.py db init                  $                  python manage.py db drift                  $                  python manage.py create_admin                              

register() view function

Finally, before we tin register a user again, nosotros need to make a quick alter to the register() view function in project/user/views.py

Change:

                                                  user                  =                  User                  (                  email                  =                  form                  .                  email                  .                  data                  ,                  password                  =                  grade                  .                  password                  .                  data                  )                              

To:

                                                  user                  =                  User                  (                  e-mail                  =                  course                  .                  email                  .                  data                  ,                  password                  =                  form                  .                  password                  .                  information                  ,                  confirmed                  =                  Simulated                  )                              

Brand sense? Call up about why we would want to default confirmed to False.

Okay. Run the app once again. Navigate to http://localhost:5000/register and register a new user again. If you lot open up your SQLite database in the SQLite Browser, you should see:

SQLite browser: user registration

So, the new user that I registered, michael@realpython.com, is non confirmed. Permit'southward modify that.

Add email confirmation

Generate confirmation token

The email confirmation should contain a unique URL that a user simply needs to click in order to ostend his/her account. Ideally, the URL should await something similar this - http://yourapp.com/confirm/<id>. The key here is the id. We are going to encode the user email (along with a timestamp) in the id using the itsdangerous packet.

Create a file called project/token.py and add the following 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                  ,                  common salt                  =                  app                  .                  config                  [                  'SECURITY_PASSWORD_SALT'                  ])                  def                  confirm_token                  (                  token                  ,                  expiration                  =                  3600                  ):                  serializer                  =                  URLSafeTimedSerializer                  (                  app                  .                  config                  [                  'SECRET_KEY'                  ])                  effort                  :                  e-mail                  =                  serializer                  .                  loads                  (                  token                  ,                  table salt                  =                  app                  .                  config                  [                  'SECURITY_PASSWORD_SALT'                  ],                  max_age                  =                  expiration                  )                  except                  :                  return                  False                  return                  email                              

So, in the generate_confirmation_token() part nosotros apply the URLSafeTimedSerializer to generate a token using the email accost obtained during user registration. The actual email is encoded in the token. So to ostend the token, within the confirm_token() function, nosotros can use the loads() method, which takes the token and expiration - valid for 1 60 minutes (3,600 seconds) - as arguments. Equally long as the token has not expired, so it will return an electronic mail.

Be sure to add together the SECURITY_PASSWORD_SALT to your app'southward config (BaseConfig()):

                                                  SECURITY_PASSWORD_SALT                  =                  'my_precious_two'                              

Update annals() view function

At present let'due south update the register() view office once again from project/user/views.py:

                                                  @user_blueprint                  .                  route                  (                  '/register'                  ,                  methods                  =                  [                  'Get'                  ,                  'Mail service'                  ])                  def                  register                  ():                  form                  =                  RegisterForm                  (                  request                  .                  form                  )                  if                  grade                  .                  validate_on_submit                  ():                  user                  =                  User                  (                  e-mail                  =                  course                  .                  electronic mail                  .                  data                  ,                  password                  =                  course                  .                  password                  .                  information                  ,                  confirmed                  =                  False                  )                  db                  .                  session                  .                  add                  (                  user                  )                  db                  .                  session                  .                  commit                  ()                  token                  =                  generate_confirmation_token                  (                  user                  .                  email                  )                              

Also, make sure to update the imports:

                                                  from                  projection.token                  import                  generate_confirmation_token                  ,                  confirm_token                              

Handle Email Confirmation

Next, let's add together a new view to handle the email confirmation:

                                                  @user_blueprint                  .                  route                  (                  '/ostend/<token>'                  )                  @login_required                  def                  confirm_email                  (                  token                  ):                  endeavour                  :                  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. Delight 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                  (                  'primary.dwelling'                  ))                              

Add this to project/user/views.py. Also, exist sure to update the imports:

Here, we call the confirm_token() function, passing in the token. If successful, we update the user, irresolute the email_confirmed attribute to Truthful and setting the datetime for when the confirmation took identify. Likewise, in case the user already went through the confirmation process - and is confirmed - and then nosotros alert the user of this.

Create the email template

Next, let's add a base email template:

                                                  <                  p                  >Welcome! Thank you for signing up. Please follow this link to actuate your business relationship:</                  p                  >                  <                  p                  ><                  a                  href                  =                  "{{ confirm_url }}"                  >{{ confirm_url }}</                  a                  ></                  p                  >                  <                  br                  >                  <                  p                  >Cheers!</                  p                  >                              

Save this equally actuate.html in "projection/templates/user". This have a single variable called confirm_url, which volition be created in the register() view part.

Send email

Allow's create a basic role for sending emails with a little help from Flask-Mail, which is already installed and setup in project/__init__.py.

Create a file chosen e-mail.py:

                                                  # project/electronic mail.py                  from                  flask.ext.mail                  import                  Message                  from                  project                  import                  app                  ,                  post                  def                  send_email                  (                  to                  ,                  subject                  ,                  template                  ):                  msg                  =                  Message                  (                  field of study                  ,                  recipients                  =                  [                  to                  ],                  html                  =                  template                  ,                  sender                  =                  app                  .                  config                  [                  'MAIL_DEFAULT_SENDER'                  ]                  )                  mail                  .                  send                  (                  msg                  )                              

Save this in the "project" folder.

So, we simply need to pass a listing of recipients, a bailiwick, and a template. We'll deal with the mail configuration settings in a flake.

Update register() view function in project/user/views.py (once more!)

                                                  @user_blueprint                  .                  route                  (                  '/annals'                  ,                  methods                  =                  [                  'Go'                  ,                  'Mail'                  ])                  def                  register                  ():                  form                  =                  RegisterForm                  (                  request                  .                  class                  )                  if                  grade                  .                  validate_on_submit                  ():                  user                  =                  User                  (                  email                  =                  course                  .                  e-mail                  .                  data                  ,                  password                  =                  form                  .                  password                  .                  data                  ,                  confirmed                  =                  Faux                  )                  db                  .                  session                  .                  add together                  (                  user                  )                  db                  .                  session                  .                  commit                  ()                  token                  =                  generate_confirmation_token                  (                  user                  .                  e-mail                  )                  confirm_url                  =                  url_for                  (                  'user.confirm_email'                  ,                  token                  =                  token                  ,                  _external                  =                  True                  )                  html                  =                  render_template                  (                  'user/activate.html'                  ,                  confirm_url                  =                  confirm_url                  )                  bailiwick                  =                  "Delight ostend your email"                  send_email                  (                  user                  .                  electronic mail                  ,                  bailiwick                  ,                  html                  )                  login_user                  (                  user                  )                  flash                  (                  'A confirmation email has been sent via electronic mail.'                  ,                  'success'                  )                  return                  redirect                  (                  url_for                  (                  "main.home"                  ))                  return                  render_template                  (                  'user/register.html'                  ,                  form                  =                  course                  )                              

Add the post-obit import besides:

                                                  from                  project.email                  import                  send_email                              

Hither, we are putting everything together. This function basically acts as a controller (either direct or indirectly) for the entire process:

  • Handle initial registration,
  • Generate token and confirmation URL,
  • Send confirmation email,
  • Flash confirmation,
  • Log in the user, and
  • Redirect user.

Did you observe the _external=True argument? This adds the total absolute URL that includes the hostname and port (http://localhost:5000, in our case.)

Before we can test this out, we demand to set our mail service settings.

Mail

Beginning by updating the BaseConfig() in project/config.py:

                                                  class                  BaseConfig                  (                  object                  ):                  """Base configuration."""                  # primary config                  SECRET_KEY                  =                  'my_precious'                  SECURITY_PASSWORD_SALT                  =                  'my_precious_two'                  DEBUG                  =                  Faux                  BCRYPT_LOG_ROUNDS                  =                  xiii                  WTF_CSRF_ENABLED                  =                  Truthful                  DEBUG_TB_ENABLED                  =                  False                  DEBUG_TB_INTERCEPT_REDIRECTS                  =                  False                  # mail settings                  MAIL_SERVER                  =                  'smtp.googlemail.com'                  MAIL_PORT                  =                  465                  MAIL_USE_TLS                  =                  Fake                  MAIL_USE_SSL                  =                  True                  # gmail authentication                  MAIL_USERNAME                  =                  os                  .                  environ                  [                  'APP_MAIL_USERNAME'                  ]                  MAIL_PASSWORD                  =                  bone                  .                  environ                  [                  'APP_MAIL_PASSWORD'                  ]                  # mail accounts                  MAIL_DEFAULT_SENDER                  =                  'from@case.com'                              

Bank check out the official Flask-Mail documentation for more info.

If you lot already have a GMAIL account then you can use that or register a test GMAIL account. Then set the environment variables temporarily in the current shell session:

                                                  $                                    export                  APP_MAIL_USERNAME                  =                  "foo"                  $                                    export                  APP_MAIL_PASSWORD                  =                  "bar"                              

If your GMAIL account has 2-step authentication, Google will cake the attempt.

Now let's examination!

Showtime test

Burn down upwardly the app, and navigate to http://localhost:5000/annals. And then annals with an email address that you have access to. If all went well, yous should accept an email in your inbox that looks something similar this:

Email confirmation

Click the URL and you should exist taken to http://localhost:5000/. Make certain that the user is in the database, the 'confirmed' field is True, and in that location is a datetime associated with the confirmed_on field.

Dainty!

Handle permissions

If y'all recall, at the beginning of this tutorial, we decided that "unconfirmed users can log in but they should be immediately redirected to a page - let'south call the road /unconfirmed - reminding users that they need to ostend their business relationship before they can access the application".

So, we need to-

  1. Add the /unconfirmed road
  2. Add an unconfirmed.html template
  3. Update the register() view part
  4. Create a decorator
  5. Update navigation.html template

Add /unconfirmed route

Add the following route to project/user/views.py:

                                                  @user_blueprint                  .                  road                  (                  '/unconfirmed'                  )                  @login_required                  def                  unconfirmed                  ():                  if                  current_user                  .                  confirmed                  :                  return                  redirect                  (                  'main.home'                  )                  wink                  (                  'Delight confirm your business relationship!'                  ,                  'warning'                  )                  return                  render_template                  (                  'user/unconfirmed.html'                  )                              

Yous've seen similar code earlier, and then let'due south move on.

Add together unconfirmed.html template

                                {% extends "_base.html" %}  {% block content %}                  <                  h1                  >Welcome!</                  h1                  >                  <                  br                  >                  <                  p                  >You have not confirmed your business relationship. Please check your inbox (and your spam binder) - y'all should accept received an email with a confirmation link.</                  p                  >                  <                  p                  >Didn't become the email?                  <                  a                  href                  =                  "/"                  >Resend</                  a                  >.</                  p                  >                  {% endblock %}                              

Save this as unconfirmed.html in "project/templates/user". Again, this should all be straightforward. For at present, we merely added a dummy URL in for resending the confirmation email. We'll address this further down.

Update the register() view function

Now only change:

                                                  return                  redirect                  (                  url_for                  (                  "main.home"                  ))                              

To:

                                                  return                  redirect                  (                  url_for                  (                  "user.unconfirmed"                  ))                              

So, subsequently the confirmation email is sent, the user is now redirected to the /unconfirmed 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                              

Hither nosotros have a basic function to cheque if a user is unconfirmed. If unconfirmed, the user is redirected to the /unconfirmed road. Salvage this every bit decorators.py in the "project" directory.

Now, decorate the profile() view part:

                                                  @user_blueprint                  .                  route                  (                  '/profile'                  ,                  methods                  =                  [                  'Become'                  ,                  'POST'                  ])                  @login_required                  @check_confirmed                  def                  profile                  ():                  # ... snip ...                              

Make sure to import the decorator:

                                                  from                  projection.decorators                  import                  check_confirmed                              

Update navigation.html template

Finally, update the following role of the navigation.html template-

Change:

                                                  <                  ul                  form                  =                  "nav navbar-nav"                  >                  {% if current_user.is_authenticated() %}                  <                  li                  ><                  a                  href                  =                  "{{ url_for('user.profile') }}"                  >Profile</                  a                  ></                  li                  >                  {% endif %}                  </                  ul                  >                              

To:

                                                  <                  ul                  form                  =                  "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                  >                              

Time to exam once more!

Second examination

Burn down up the app, and register over again with an email address that you take access to. (Feel gratuitous to delete the old user that you registered earlier first from the database to employ again.) Now y'all should be redirected to http://localhost:5000/unconfirmed after registration.

Brand sure to examination the http://localhost:5000/profile route. This should redirect you to http://localhost:5000/unconfirmed.

Go alee and ostend the electronic mail, and yous will take access to all pages. Blast!

Resend electronic mail

Finally, let's get the resend link working. Add the following view role to project/user/views.py:

                                            @user_blueprint                .                road                (                '/resend'                )                @login_required                def                resend_confirmation                ():                token                =                generate_confirmation_token                (                current_user                .                email                )                confirm_url                =                url_for                (                'user.confirm_email'                ,                token                =                token                ,                _external                =                Truthful                )                html                =                render_template                (                'user/actuate.html'                ,                confirm_url                =                confirm_url                )                subject                =                "Please ostend your e-mail"                send_email                (                current_user                .                email                ,                subject                ,                html                )                wink                (                'A new confirmation e-mail has been sent.'                ,                'success'                )                render                redirect                (                url_for                (                'user.unconfirmed'                ))                          

At present update the unconfirmed.html template:

                            {% extends "_base.html" %}  {% block content %}                <                h1                >Welcome!</                h1                >                <                br                >                <                p                >You have non confirmed your account. Please bank check your inbox (and your spam folder) - you should have received an electronic mail with a confirmation link.</                p                >                <                p                >Didn't get the email?                <                a                href                =                "{{ url_for('user.resend_confirmation') }}"                >Resend</                a                >.</                p                >                {% endblock %}                          

Third examination

You know the drill. This time make sure to resend a new confirmation e-mail and examination the link. It should piece of work.

Finally, what happens if you send yourself a few confirmation links? Are each valid? Test information technology out. Register a new user, and and so ship a few new confirmation emails. Try to confirm with the get-go email. Did it work? It should. Is this okay? Practice y'all call back those other emails should elapse if a new one is sent?

Do some research on this. And test out other web applications that yous use. How do they handle such beliefs?

Update examination suite

Alright. And so that'southward it for the primary functionality. How about we update the electric current test suite since it's, well, broken.

Run the tests:

You should meet the post-obit error:

                                            TypeError: __init__() takes at to the lowest degree four arguments (3 given)                          

To correct this we simply need to update the setUp() method in project/util.py:

                                            def                setUp                (                cocky                ):                db                .                create_all                ()                user                =                User                (                e-mail                =                "ad@min.com"                ,                password                =                "admin_user"                ,                confirmed                =                False                )                db                .                session                .                add together                (                user                )                db                .                session                .                commit                ()                          

Now run the tests again. All should pass!

Conclusion

There's conspicuously a lot more than we tin can practise:

  1. Rich vs. plain text emails - We should be sending out both.
  2. Reset password electronic mail - These should be sent out for users that take forgotten their passwords.
  3. User management - We should allow users to update their emails and passwords, and when an email is inverse, it should be confirmed again.
  4. Testing - We need to write more tests to cover the new features.

Download the unabridged source code from the Github repository. Comment beneath with questions. Check out part 2.

Happy holidays!