Monday, October 9, 2023

Creating verification link and handling verification requests

With email sending configured, the next step is to send a verification link to a newly registered user. Upon clicking this verification link, the user account should become active. This can be done in the send_verification_link_email util method. To create tokens, I am using the Simple JWT package that is recommended with Django Rest Framework.

from rest_framework_simplejwt.tokens import RefreshToken

verification_token = RefreshToken.for_user(user)
verification_token.set_exp(
    from_time=verification_token.current_time,
    lifetime=timedelta(minutes=settings.EMAIL_VERIFICATION_TIMELIMIT)
)

To create the verification link, rather than format the entire URL as a string, I use a combination of Django settings and URL reverse lookups. To begin with, the method used to include the URL end points for the user_auth app was wrong. In the main urls.py file in server directory, I need to define a namespace for the user_auth app.

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('user_auth.urls', namespace='user_auth')),
]

And in the urls.py in the user_auth module, I need to define an app_name:

app_name = 'user_auth'
urlpatterns = [
    path('register-user', RegisterUserView.as_view(), name='register-user'),
    path('verify-user/', VerifyUserView.as_view(), name='verify-user'),
]

It is possible to extract the domain name along with the protocol (http or https) from the request object. But rather than pass the request object to a utility function, the base domain name along with the protocol can be a setting parameter which will change with the environment. So, in the settings.py file:

# Base URL
BASE_URL = KEYS.BASE_URL

And in the enviromment, this BASE_URL can be defined as http://localhost:8000 which is the development server. Once this is done, using string formatting and URL reverse lookups, the verification link can be inserted into email message.

message_body = (
    "Hello,\n"
    "Thank you for registering with Online Edu!\n"
    "\n"
    "You are not yet ready to use your account.
    Before you can login to the website, 
    please verify your email by clicking on this link:\n"
    "{base_url}{token_url} \n"
    "\n"
    "Please click on this link within 15 minutes of receiving this email.\n"
    "\n"
    "Thank you,\n"
    "Online Edu"
).format(
    base_url=settings.BASE_URL,
    token_url=reverse(
        'user_auth:verify-user',
        args=[verification_token]
    )
)

With this, the amount of hardcoding is the least and with changes in deployment or for that matter in the URL end-points, no changes will need to be made in the utility method that sends the email.

To handle the verification, we need another view class. From the Simple JWT documentation, there are ready made views that extract the token and verify it. However, in my case, I need to verify the token and if verified, the new user account needs to be set to active. Sadly, the Simple JWT documentation is very scant and to figure out all that can be done by this package, it is necessary to browse through the Github code. For now, this view class will do as clicking the link is a GET request with the token as a URL parameter rather than embedded in the header which would be the case with more API requests. Therefore, a lot of the code is custom, though it might be possible to replace it later with boilerplate code.

class VerifyUserView(APIView):
    '''Checks token received on clicking verification link'''

    def get(self, *args, **kwargs):
        verification_token = self.kwargs['token']
        if not verification_token:
            return Response(
                data='Missing token',
                status=status.HTTP_400_BAD_REQUEST
            )
        token_data = TokenRefreshSerializer(
            data={'refresh': verification_token}
        )
        try:
            if not token_data.is_valid():
                error_list = [token_data.errors[e][0].title()
                              for e in token_data.errors]
                return Response(
                    data=error_list[0],
                    status=status.HTTP_400_BAD_REQUEST
                )
            else:
                # Set the user to active
                user_data = RefreshToken(verification_token)
                user_obj = User.objects.get(id=int(user_data['user_id']))
                user_obj.is_active = True
                user_obj.save()
        except Exception as e:
            return Response(
                data=str(e),
                status=status.HTTP_400_BAD_REQUEST
            )
        return Response(status=status.HTTP_200_OK)

If no token is passed to the URL end point or if the token is invalid or has been tampered with, the view will return a 400 bad request response. There is a TokenRefreshSerializer serializer in Simple JWT which takes the token as data. This then has a validation method which checks for expiry and also things like blacklisting of tokens. Though this is a serializer, it does not seem possible to extract the .data attribute and then check the payload for the user information. To be able to do this, I import the RefreshToken model from SimpleJWT and pass the verification token to it. This allows to extract the user_id from the payload. This is necessary to set the is_active to True.

This is the first task that has resulted in me thinking about JWTs and how they can be used. Specifically, how to handle an expired token and what can be done with it. This is to be handled on the frontend as a user who clicks on the verification link after it expires will need a refresh options.

No comments:

Post a Comment