Send reset password link with Ionic and Django - Part two

September 2019

Reset password link in Django

First thing to do is to declare our url to receive the password reset demand. To do this, we modify the urls.py file in our bikebackend directory to add:

path('account/reset_password', sendPasswordLink, name="reset_password"),

Which means that when the account/reset_password url is called, Django will execute the sendPasswordLink method. This method will be declare in a new directory that we will create to manage the full functionnality of reseting password. The full urls.py file should be:

 from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from django.conf import settings
from django.conf.urls.static import static
from rest_framework.routers import DefaultRouter
from resetpassword.utils.views import sendPasswordLink
router = DefaultRouter()
urlpatterns = [
    path('', include(router.urls)),
    path('jet/', include('jet.urls', 'jet')),  # Django JET URLS
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    path('doc/', include('rest_framework_docs.urls')),
    path('o/', include('oauth2_provider.urls')),
    path('account/reset_password', sendPasswordLink, name="reset_password"),
]+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 

So we are going to create this resetpassword package. At the root of our bikebackend project, we create a new directory named resetpassword and inside the directory, we create an empty file __init__.py because we want it to be interpreted as a python package.

mkdir resetpassword
cd resetpassword
touch __init__.py 

Then inside our directory we will create a new python directory named utils which will contains our python code

mkdir utils
touch utils/__init__.py
touch utils/views.py

Now we can create our sendPasswordLink method inside the views.py file.

@csrf_exempt
def sendPasswordLink(request):
    if request.method == "POST":
        data = request.POST['email_or_username']
        associated_users = User.objects.filter(email=data)
        try:
         if associated_users.exists():
                for user in associated_users:
                    reset_password(user, request)
                return HttpResponse(status=200)
        except Exception as e:
                print(e)
        return HttpResponse(status=404)
    else:
        return HttpResponse(status=404)

Before our method we write the @csrf_exempt annotation because our data is coming from a post request sent by our Ionic application. There is no need of CSRF django protection (more info about CRSF)
Then we get the email sent from the ionic application, in the post request and we verify if we can find a user in our database with the provided email. If a user is found, we can send him the requested link for resetting his password, otherwise we send back an http error 404 to our ionic application.

def reset_password(user, request):
    c = {
        'email': user.email,
        'domain': request.META['HTTP_HOST'],
        'site_name': 'bikebackend',
        'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode('utf-8'),
        'user': user,
        'token': default_token_generator.make_token(user),
        'protocol': 'http',
    }
    subject_template_name = 'registration/password_reset_subject.txt'
    email_template_name = 'registration/password_reset_email.html'
    subject = loader.render_to_string(subject_template_name, c)
    # Email subject *must not* contain newlines
    subject = ''.join(subject.splitlines())
    email = loader.render_to_string(email_template_name, c)
    try:
        to_email = Email(user.email)
        from_email = Email("contact@bikebackend.fr")
        content = Content('text/html', email)
        message = Mail(from_email, subject, to_email, content)
        sg = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY)
        sg.client.mail.send.post(request_body=message.get())
    except Exception as e:
        print(e.message)

The reset_password method will generate a unique token to add it in the link that we will sent.
The subject of the email will use the defaut django contrib admin file registration/password_reset_subject.txt and we will override the default registration/password_reset_email.html file to add our own content.

To send the email, we will use the sendgrid services and more particulary the python sdk (the SENDGRID_API_KEY will be declare in our settings.py file)
We don't forget to add sendgrid to our requirements.txt file

django>=2.0.11
psycopg2==2.7 --no-binary psycopg2
pillow==5.1.0
django-jet
djangorestframework
django-filter
git+https://github.com/mago960806/django-rest-framework-docs.git
django-oauth-toolkit
django-cors-middleware
sendgrid

Here is the full views.py file (with import)

# -*-coding:utf-8 -*-
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.template import loader
from bikebackend.settings import SENDGRID_API_KEY
from backoffice.models import User
from django.http import HttpResponse
import sendgrid
from django.views.decorators.csrf import csrf_exempt
from sendgrid.helpers.mail import Mail,Email,Content
def reset_password(user, request):
    c = {
        'email': user.email,
        'domain': request.META['HTTP_HOST'],
        'site_name': 'bikebackend',
        'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode('utf-8'),
        'user': user,
        'token': default_token_generator.make_token(user),
        'protocol': 'http',
    }
    subject_template_name = 'registration/password_reset_subject.txt'
    email_template_name = 'registration/password_reset_email.html'
    subject = loader.render_to_string(subject_template_name, c)
    # Email subject *must not* contain newlines
    subject = ''.join(subject.splitlines())
    email = loader.render_to_string(email_template_name, c)
    try:
        to_email = Email(user.email)
        from_email = Email("contact@bikebackend.fr")
        content = Content('text/html', email)
        message = Mail(from_email, subject, to_email, content)
        sg = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY)
        sg.client.mail.send.post(request_body=message.get())
    except Exception as e:
        print(e.message)
@csrf_exempt
def sendPasswordLink(request):
    if request.method == "POST":
        data = request.POST['email_or_username']
        associated_users = User.objects.filter(email=data)
        try:
         if associated_users.exists():
                for user in associated_users:
                    reset_password(user, request)
                return HttpResponse(status=200)
        except Exception as e:
                print(e)
        return HttpResponse(status=404)
    else:
        return HttpResponse(status=404)

We need to create our registration/password_reset_email.html file. This file must be created inside a templates directory inside our resetpassword directory

mkdir templates
mkdir templates/registration
touch templates/registration/password_reset_email.html

And our html file will contains :

{% load i18n %}{% autoescape off %}
Hi,
<br><br>
You are receiving this email because of your reset password request.
<br><br>
Please go to this page and choose a new password:
    {% block reset_link %}
        http://{{ domain }}/account/reset_password_confirm/{{uid}}/{{token}}
    {% endblock %}
<br><br>
The BikeBackend Team
{% endautoescape %}

Inside our html file, we are getting the values passed our django context declared in our views. With these values we are able to create a dynamic link on which the user will have to click to go and choose a new password.
So now we need to define this new url in our urls.py file

re_path('account/reset_password_confirm/(?P<uidb64>[0-9A-Za-z]+)/(?P<token>.+)',PasswordResetConfirmView.as_view(), name='password_reset_confirm')

and the associated method in our views.py file.

class PasswordResetConfirmView(FormView):
    template_name = "account/pwdoublie.html"
    success_url = '/account/pwdenvoye'
    form_class = SetPasswordForm
    def post(self, request, uidb64=None, token=None, *arg, **kwargs):
        """
        View that checks the hash in a password reset link and presents a
        form for entering a new password.
        """
        form = self.form_class(request.POST)
        assert uidb64 is not None and token is not None  # checked by URLconf
        try:
            uid = urlsafe_base64_decode(uidb64)
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None
        if user is not None and default_token_generator.check_token(user, token):
            if form.is_valid():
                new_password = form.cleaned_data['confirmation']
                user.password = hashlib.sha256(new_password.encode('utf8')).hexdigest()
                user.save()
                messages.success(request, 'Your password has been modified')
                return self.form_valid(form)
            else:
                print(form.errors)
                messages.error(request, form.errors)
                return self.form_invalid(form)
        else:
            messages.error(
                request, 'This link has expired.')
            return self.form_invalid(form)

This method will display an html page (pwdoublie.html) containing our form asking for new password with a submit form button. The form will be valided using the SetPasswordForm declared in a forms.py file

from django import forms
class PasswordResetRequestForm(forms.Form):
    email_or_username = forms.CharField(label=("Email Or Username"), max_length=254)
class SetPasswordForm(forms.Form):
    """
    A form that lets a user change set their password without entering the old
    password
    """
    error_messages = {
        'password_mismatch': ("Passwords doesn't match."),
        'password_length': ("Your password must be 8 caracters minimum."),
        }
    new_password1 = forms.CharField(label=("New password"),
                                    widget=forms.PasswordInput)
    confirmation = forms.CharField(label=("Confirm new password"),
                                    widget=forms.PasswordInput)
    def clean_confirmation(self):
        password1 = self.cleaned_data.get('new_password1')
        password2 = self.cleaned_data.get('confirmation')
        if password1 and password2:
            if password1 != password2:
                raise forms.ValidationError(
                    self.error_messages['password_mismatch'],
                    code='password_mismatch',
                    )
            if len(password1)<8:
                raise forms.ValidationError(
                    self.error_messages['password_length'],
                    code='password_length',
                )
        return password2

If everything is ok, we can redirect the user to /account/pwdenvoye url which will display a successful page and telling him that i can go back to the ionic application to login.

path('account/pwdenvoye', pwdenvoye, name="pwdenvoye"),

and the method in the views.py

def pwdenvoye(request):
    return render(request,'account/pwdmodifie.html',{})

As always you can find the source code here