The Confirmation Url Is Invalid or Has Already Been Used Please Signup Again
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:
            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.
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:
          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-
- Add the              
/unconfirmedroad - Add an unconfirmed.html template
 - Update the              
register()view part - Create a decorator
 - 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                                          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:
- Rich vs. plain text emails - We should be sending out both.
 - Reset password electronic mail - These should be sent out for users that take forgotten their passwords.
 - User management - We should allow users to update their emails and passwords, and when an email is inverse, it should be confirmed again.
 - 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!
Source: https://realpython.com/handling-email-confirmation-in-flask/
0 Response to "The Confirmation Url Is Invalid or Has Already Been Used Please Signup Again"
Postar um comentário