Django Email Verification
Do you like my work and want to support me?
Requirements
- Python >= 3.8
- Django >= 4.2
General concept
Here is a simple Sequence Diagram of the email verification process:
sequenceDiagram
actor U as User
participant D as django-email-verification
participant C as Your Code
U -->> C: Creates an Account
note over C: Set User as inactive
C ->> D: Call send_email
D -)+ U: Email with Activation Link
U -)- C: Link clicked
C ->> D: Request forwarded
critical Token Validation
option Valid
D ->> C: Run Callback
D ->> U: Render Success Page
option Invalid
D ->> U: Render Error Page
end
And here is a simple Sequence Diagram of the password recovery process:
sequenceDiagram
actor U as User
participant D as django-email-verification
participant C as Your Code
U -->> C: Click on Recover Password
C ->> D: Call send_password
D -)+ U: Email with Password Change Link
U -)- C: Link clicked
C ->> D: Request forwarded
critical Token Validation
option Valid
D ->> U: Render Password Change View
U ->> D: Submit new Password
D ->> C: Run Callback
D ->> U: Render Success Page
option Invalid
D ->> U: Render Error Page
end
The app is build to be as little opinionated as possible, every action it can perform can be replaced by custom code, and everything else will continue working just the same.\ For both Email Verification and Password Recovery, the features can be divided into:
Installation
You can install by:
pip3 install django-email-verification
and import by:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
...
'django_email_verification', # you have to add this
]
and add the following to your urls.py
file:
urlpatterns = [
...
path('verify/', include('django_email_verification.urls')),
...
]
Settings parameters
You have to add these parameters to the settings, you have to include all of them except the last one:
# settings.py
def email_verified_callback(user):
user.is_active = True
def password_change_callback(user, password):
user.set_password(password)
# Global Package Settings
EMAIL_FROM_ADDRESS = '[email protected]' # mandatory
EMAIL_PAGE_DOMAIN = 'https://mydomain.com/' # mandatory (unless you use a custom link)
EMAIL_MULTI_USER = False # optional (defaults to False)
# Email Verification Settings (mandatory for email sending)
EMAIL_MAIL_SUBJECT = 'Confirm your email {{ user.username }}'
EMAIL_MAIL_HTML = 'mail_body.html'
EMAIL_MAIL_PLAIN = 'mail_body.txt'
EMAIL_MAIL_TOKEN_LIFE = 60 * 60 # one hour
# Email Verification Settings (mandatory for builtin view)
EMAIL_MAIL_PAGE_TEMPLATE = 'email_success_template.html'
EMAIL_MAIL_CALLBACK = email_verified_callback
# Password Recovery Settings (mandatory for email sending)
EMAIL_PASSWORD_SUBJECT = 'Change your password {{ user.username }}'
EMAIL_PASSWORD_HTML = 'password_body.html'
EMAIL_PASSWORD_PLAIN = 'password_body.txt'
EMAIL_PASSWORD_TOKEN_LIFE = 60 * 10 # 10 minutes
# Password Recovery Settings (mandatory for builtin view)
EMAIL_PASSWORD_PAGE_TEMPLATE = 'password_changed_template.html'
EMAIL_PASSWORD_CHANGE_PAGE_TEMPLATE = 'password_change_template.html'
EMAIL_PASSWORD_CALLBACK = password_change_callback
# For Django Email Backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'mYC00lP4ssw0rd' # os.environ['password_key'] suggested
EMAIL_USE_TLS = True
For simplicity, I will refer to both XX_MAIL_XX
and XX_PASSWORD_XX
by writing XX_{MAIL|PASSWORD}_XX
.
In detail:
EMAIL_FROM_ADDRESS
: this can be the same asEMAIL_HOST_USER
or an alias address if required.EMAIL_PAGE_DOMAIN
: the domain of the confirmation link (usually your site's domain).EMAIL_MULTI_USER
: (optional) ifTrue
an error won't be thrown if multiple users with the same email are present ( just one will be activated)EMAIL_MAIL_CALLBACK
: will be called when the user successfully verifies the email. Can be a function (taking the user object as a parameter) or a method on the user object (no arguments) 1.EMAIL_PASSWORD_CALLBACK
: will be called when the user successfully submits a new password. Can be a function (taking the user object and the new password as parameters) or a method on the user object (taking the new password as a parameter)1.EMAIL_{MAIL|PASSWORD}_
: are all django templates:SUBJECT
: the mail default subject.HTML
: the mail body template in form of html.PLAIN
: the mail body template in form of .txt file.
EMAIL_{MAIL|PASSWORD}_TOKEN_LIFE
: the lifespan of the email link (in seconds).EMAIL_{MAIL|PASSWORD}_PAGE_TEMPLATE
: the template of the success/error view. Takes{success: bool, user: Model, request: WSGIRequest}
as parameters.EMAIL_PASSWORD_CHANGE_TEMPLATE
: the template for the page with the form to submit a new password. Must send a POST request to the same address, with the fieldpassword
in the payload.
For the Django Email Backend fields look at the official documentation.
Email Sending
The functions in charge of sending the emails are the following:
send_email(user, thread=True, expiry=None, context=None)
send_password(user, thread=True, expiry=None, context=None)
The fields are:
- user
(Model
): the user you want to send the email to
- thread
(bool
): whether to send the email asynchronously or not
- expiry
(datetime
): custom token expiry date (different from datetime.now() + EMAIL_{MAIL|PASSWORD}_TOKEN_LIFE
)
- context
(dict
): additional context for the email template
NOTE: By default the email is sent asynchronously, which is the suggested behaviour, if this is a problem (for example if you are running synchronous tests), you can pass the parameter
thread=False
.
# views.py
from django.shortcuts import render
from django.contrib.auth import get_user_model
from django_email_verification import send_email
def create_account_functional_view(request):
...
user = get_user_model().objects.create(username=username, password=password, email=email)
user.is_active = False # Example
send_email(user)
return render(...)
def recover_password_functional_view(request):
...
send_password(user)
return render(...)
send_email(user)
and send_password(user)
send an email with the defined template (and the pseudo-random generated token) to the user.
IMPORTANT: For email verification, you have to manually set the user to inactive before sending the email.
If you are using class based views, then it is necessary to call the superclass before calling the send_email
method.
# views.py
from django.views.generic.edit import FormView
from django_email_verification import send_email
class CreateAccountClassView(FormView):
def form_valid(self, form):
user = form.save()
return_val = super(CreateAccountClassView, self).form_valid(form)
send_email(user)
return return_val
Templates examples
The EMAIL_{MAIL|PASSWORD}_SUBJECT
is a template that receives {{ token }}
(str
), {{ link }}
(str
), {{ expiry }}
(datetime
) and user
(Model
) (plus your custom context) as arguments,
it might look something like this:
EMAIL_MAIL_SUBJECT = 'Confirm your email {{ user.username }}'
EMAIL_PASSWORD_SUBJECT = 'Change password request for {{ user.username }}'
The EMAIL_{MAIL|PASSWORD}_HTML
is a template that receives {{ token }}
(str
), {{ link }}
(str
), {{ expiry }}
(datetime
) and user
(Model
) (plus your custom contex) as arguments,
it might look something like this:
<h1>You are almost there, {{ user.username }}!</h1><br>
<h2>Please click <a href="{{ link }}">here</a> to confirm your account</h2>
<h2>The token expires on {{ expiry|time:"TIME_FORMAT" }}</h2>
The EMAIL_{MAIL|PASSWORD}_PLAIN
is a template that receives {{ token }}
(str
), {{ link }}
(str
), {{ expiry }}
(datetime
) and user
(Model
) (plus your custom contex) as arguments,
it might look something like this:
You are almost there, {{ user.username }}!
Please click the following link to confirm your account: {{ link }}
The token expires on {{ expiry|time:"TIME_FORMAT" }}
Verification / Recovery View
Builtin Method
The easiest way to recieve the token is to use the builtin views. To do so you just need to include the application's urls and define the necessary Django templates.
# urls.py
from django.contrib import admin
from django.urls import path, include
from django_email_verification import urls as email_urls # include the urls
urlpatterns = [
...
path('email/', include(email_urls)), # connect them to an arbitrary path
...
]
When a request arrives to https.//mydomain.com/email/email/<token>
the package verifies the token and:
+ if it corresponds to a pending token it renders the EMAIL_MAIL_PAGE_TEMPLATE
passing success=True
+ if it doesn't correspond it renders the EMAIL_MAIL_PAGE_TEMPLATE
passing success=False
If the token is correct, EMAIL_MAIL_CALLBACK
is called before the page is returned.
The EMAIL_MAIL_PAGE_TEMPLATE
is a template that receives {{ success }}
(bool
), {{ user }}
(Model
) and {{ request }}
(WSGIRequest
) as arguments,
it might look something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Confirmation</title>
</head>
<body>
{% if success %}
{{ user.username }}, your account is confirmed!
{% else %}
Error, invalid token!
{% endif %}
</body>
</html>
When a request arrives to https.//mydomain.com/email/password/<token>
the package renders EMAIL_PASSWORD_CHANGE_TEMPLATE
.
This view should present a form that submits a POST request to the same url, passing a password
field in the body.
The EMAIL_PASSWORD_CHANGE_TEMPLATE
is a template that receives {{ user }}
(Model
) and {{ request }}
(WSGIRequest
) as arguments,
it might look something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Password Change</title>
</head>
<body>
{{ user.username }}, set your new password:
<form method="post">
<label for="password">New Password:</label>
<input type="password" id="password" name="password">
<input type="submit" value="Submit">
</form>
</body>
</html>
Once the POST request it's submitted, the server verifies the token and:
+ if it corresponds to a pending token it renders the EMAIL_PASSWORD_TEMPLATE
+ if it doesn't correspond it renders the EMAIL_PASSWORD_TEMPLATE
passing success=False
If the token is correct, EMAIL_PASSWORD_CALLBACK
is called before the page is returned.
The EMAIL_MAIL_PAGE_TEMPLATE
is a template that receives {{ success }}
(bool
), {{ user }}
(Model
) and {{ request }}
(WSGIRequest
) as arguments,
it might look something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Password Changed</title>
</head>
<body>
{% if success %}
{{ user.username }}, your password has been changed!
{% else %}
Error, invalid token!
{% endif %}
</body>
</html>
Custom View Method
If you want to use your custom Django view for the verification of the token (if you need a more complex behaviour) you can do the following:
- Add your view to the
urls.py
file, using the correct url argument - Mark your view using the corresponding decorator
- Call the token verification function
Here is the code:
# urls.py
from django.urls import path
from .views import confirm_view, password_view
urlpatterns = [
...
path('email/<str:token>/', confirm_view), # remember to set the "token" parameter in the url!
path('password/<str:token>/', password_view), # remember to set the "token" parameter in the url!
...
]
IMPORTANT: the path must NOT have the
name
attribute set
# views.py
from django.http import HttpResponse
from django_email_verification import verify_email, verify_password, verify_email_view, verify_password_view
@verify_email_view
def confirm_view(request, token):
success, user = verify_email(token)
return HttpResponse(f'Account verified, {user.username}' if success else 'Invalid token')
@verify_password_view
def password_view(request, token):
if request.method == 'POST' and (pwd := request.POST.get('password')) is not None:
success, user = verify_password(token, pwd)
return HttpResponse(f'Password Changed, {user.username}' if success else 'Invalid token')
return HttpResponse('Wrong Method')
The decorators allow the app to automatically generate a url with the correct link to the view, as long as there is only one view per decorator and it has the correct arguments.
The functions verify_email(token)
and verify_password(token, password)
verify the token and, if it is correct, call the corresponding callback (EMAIL_MAIL_CALLBACK
and EMAIL_PASSWORD_CALLBACK
respectively).
Manual Token Verification
If you only need to check the token, you can use the following code:
from django_email_verification import default_token_generator
valid, user = default_token_generator.check_token(token, kind='MAIL') # For an email token
valid, user = default_token_generator.check_token(token, kind='PASSWORD') # For a password token
Testing
If you are using django-email-verification and you want to test the email, if settings.DEBUG == True, then two items will be added to the email headers. You can obtain these by checking the django.core.mail outbox, which will have a non-zero length if an email has been sent. Retrieve the email and obtain the link (includes token) or the token to use in your code.
from django.core import mail
...
test body
...
try:
email = mail.outbox[0]
link = mail.extra_headers['LINK']
token = mail.extra_headers['TOKEN']
browser.visit(link) # verifies token...
except AttributeError:
logger.warn("no email")
For the email to be in the inbox, you will need to use the correct email backend. Use either:
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
or:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
You can use any Django email backend and also your custom one.
If you want to run the builtin tests, clone the project and execute:
coverage run --source=django_email_verification -m pytest && coverage report -m
(You will need coverage, pytest and pytest-django)
Logo copyright:
Logo by Filippo Veggo
Usage of the Django trademarks are subject to the Django Trademark licensing Agreement."