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
/unconfirmed
road - 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