Skip to content

Musings of an Anonymous Geek

Made with only the finest 1's and 0's

Menu
  • About
  • Search Results
Menu

User Activation With Django and Djoser

Posted on May 6, 2021May 6, 2021 by jonesy

Depending on the project, Django and Djoser can go really well together. Django provides such an enormous feature set as a foundation, and such a modular platform, that tools like Djoser can provide enormous value while still staying out of the way of the rest of your application. At the same time, the whole solution (Django, Djoser, and any other reusable Django app I’ve ever seen), top to bottom, is Just Pythonâ„¢. That means you can almost always find the right place to hook in your own code, without having to take over responsibility for an entire solution.

Djoser is a library that integrates nicely into a Django REST Framework project to provide API endpoints for things like user registration, login and logout, password resets, etc. It also integrates pretty seamlessly with Django-SimpleJWT to enable JWT support, and will expose JWT create, refresh, and verify endpoints if support is turned on. It’s pretty sweet.

Well…. most of the time.

The only real issues I’ve had with Djoser always root from one of two assumptions the project makes:

  1. That you’re puritanical in your adherence to REST principles at every turn, and
  2. That you’re building a Single Page Application (SPA)

That first one is easily forgivable: if you’re going to be an opinionated solution, it’s best to be consistent, and strict. The minute you fall off of that wagon, everything starts to devolve into murkiness. It doesn’t seem like a big leap to say that a lot of developers prefer an API that is clear and consistent over one that is vague and inconsistent.

As for the second assumption, it honestly doesn’t get in the way very often, but on a recent project, it bit me pretty hard. On this project, I had to leverage Djoser in my Django project’s user activation flow.

User Registration and User Activation

User activation happens as part of the user registration process in my case (and, I suspect, most cases). At a high level, the registration flow goes like this:

  1. a POST is sent to the server requesting that a given username or email be given a user account, using the given password.
  2. the server generates a token of some kind, uses that token to generate a verification link, and sends an account activation email containing the link.
  3. the end user opens the email and clicks the link
  4. magic
  5. the user is activated, and may or may not get an email confirming that their account is ready to go

All of this is straightforward until you get to step 4: ‘magic’. Big surprise, right? Also perhaps unsurprising is that this is where Djoser’s assumptions make life difficult if you’re not building a Single Page Application, and/or are not a REST purist.

Djoser User Registration

Before getting to activation, you have to register. For completeness, it’s worth pointing out that Djoser provides an endpoint that takes a POST request with the desired username, email, and password to kick things off. It integrates nicely with the rest of your application without any real work to do other than adding a urlpattern that’s given to you to your urls.py file.

In my case, I’m using a custom user model and I changed the USERNAME_FIELD to ’email’, and as a result, Djoser accepts just the email and password fields by default, because it’s leaning on the base Django functionality for as much as possible, which is smart and makes everyones’ lives easier.

When this POST request comes in, password validators and any other things you have set up to happen at user creation time will happen, including the creation of a user record. However, the is_active flag will be False for that record. Then it generates an encoded uid (from the record it created) and a verification token, uses them along with the value of ACTIVATION_URL to form a confirmation link, puts that in an email to the email address used to register, and sends it. And that leads us to…

Djoser User Activation

First, let’s have a look at the default value for Djoser’s ACTIVATION_URL setting. This setting determines the URL that will be emailed to the person who is trying to register a new account. The default value is `

'#/activate/{uid}/{token}'`

This is a front end URL with placeholders for the uid and token values. It gets assembled into an account registration verification link that looks like this:

http://localhost:8000/#/activate/Mw/am6c7b-85f2acbaf4691e9cc6c891bbc4fd7754

Up to this point in my project, I was gleefully following along with what Djoser seems to be making easy for me. Then I clicked the above link and everything crashed. Why? Because Djoser does not have a back end view to handle the front end URL that is the default ACTIVATION_URL.

Here’s the explanation from one of the project maintainers: https://github.com/sunscrapers/djoser/issues/14

I suppose you directly use an url to activation view which expects POST request. When you open a link to this view in browser it makes a GET request which is simply not working.

Our assumption is that GET requests should not change the state of application. That’s why the activation view expects POST in order to affect user model. Moreover it’s REST API so if you open one of the endpoints in your browser it displays JSON response which is not something for regular user.

If you’re working on single page application you need to create a new screen with separate url that generates POST request to your REST API.

If you really want to have view that activates user on GET request then you need to implement your own view, but remember to provide reasonable html response.

To boil this down, my understanding from this is that:

  1. Djoser devs know that the ACTIVATION_URL is going to be used to create a link that is sent to someone via email.
  2. Djoser devs know that, when you click a link in an email, the result is a GET request to the back end.
  3. Djoser devs have provided a view for user activation that only supports POST requests in spite of this fact.

This is immensely frustrating. Their implementation seems to sacrifice a product that actually works at all for the sake of REST purity and maybe an assumption that all developers are only creating SPAs. What’s more, searching around for solutions turns up lots of confused people. The solution that I found trending was one where you write your own view that does accept a GET request, and then, inside the view, in the back end code, make a POST request to run the code in Djoser’s UserActivationView!

This all just felt way too… wrong for me. The back end should not make an HTTP request to itself. Perhaps what I did wasn’t 100% perfect either, but I’d be interested in a dialog that could shed more light on why things are the way they are, and how to properly and effectively deal with it.

My Workaround

First, if you’re just here for the code, here’s the view I created and the urlpattern that maps to it.

from djoser.views import UserViewSet
from rest_framework.response import Response

class ActivateUser(UserViewSet):
    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())

        # this line is the only change from the base implementation.
        kwargs['data'] = {"uid": self.kwargs['uid'], "token": self.kwargs['token']}

        return serializer_class(*args, **kwargs)

    def activation(self, request, uid, token, *args, **kwargs):
        super().activation(request, *args, **kwargs)
        return Response(status=status.HTTP_204_NO_CONTENT)

My Djoser ACTIVATION_URL in settings.py is `

'accounts/activate/{uid}/{token}'

And then the urlpattern used to map requests to that url looks like this:

path('accounts/activate/<uid>/<token>', ActivateUser.as_view({'get': 'activation'}), name='activation'),

What’s Actually Happening In My Workaround

Djoser leans on Django and Django Rest Framework for a lot of its functionality. In order to support a large number of URLs while duplicating the least amount of code, Djoser utilizes Django Rest Framework’s ‘ViewSet’ concept, which lets you map an ‘action’ to a method in a single class. So, instead of having separate views for “UserRegistration”, “UserActivation”, “UserPasswordChange”, and all of the other things that can happen to a user, Djoser just has one class called “UserViewSet” (at djoser.views.UserViewSet).

UserViewSet.activation is a method that takes a POST request containing the UID and token values, validates them, and (assuming validation passes) sends a signal that, in my application, sets the is_active flag on the user to True and sends the new user an email letting them know their account is now active. “It’s all perfect except for the POST!” I thought. But I wasn’t happy with solutions that have code in the back end going back out to the internet to trigger other code on the back end. I wasn’t going to accept sending a POST request to trigger another view.

So, step one was to create my own view, inheriting from UserViewSet, and then allowing that view to accept a GET request, because you’ll recall that our mission is to handle the user clicking the link in their email to activate their account. Aside from accepting a GET request, I don’t really want my code to do anything at all. Just call super().activation and get out of the way!

Now, the base implementation forces POST-only by decorating it with an @action decorator. The first argument to the decorator is a list of HTTP methods supported, and only post is listed. Great! So just don’t decorate that method, map it to a GET in urls.py, and you’re all set!

Sadly, it was not quite that easy. Since UserViewSet.activation only supports a POST request, it also assumes that what it needs is already in request.data. But when our GET request comes in, the uid and token values will be in kwargs. Making things more difficult, the request object here is Django Rest Framework’s Request object, and its data attribute is not settable (I think it’s a property defined with no setter, but don’t quote me). So, I can’t just overwrite request.data and move on. Now what?

So, we need to find a way to get data into request.data so that when I call super().activation(), it can act on that data. In looking at the code for UserViewSet.activation I found this:

    @action(["post"], detail=False)
    def activation(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

That’s not the whole method, but for the whole method, the only time request.data is referenced is on the very first line of the code. Since we already said I can’t just shim in a line and overwrite request.data, let’s instead have a look at this get_serializer method!

    def get_serializer(self, *args, **kwargs):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

Notice that it doesn’t explicitly have a data parameter defined in the method’s signature. That means any reference to data would have to be in kwargs. That means we can set kwargs['data'] inside of this method to whatever we want. The updated method to make that happen only adds a single line:

    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        kwargs['data'] = {"uid": self.kwargs['uid'], "token": self.kwargs['token']}

        return serializer_class(*args, **kwargs)

That’s it. You just effectively replaced request.data.

One More Time

This is a lot. Let’s review what happened.

First, the mission:

  • Support a GET request that happens when the user clicks the account activation link in their email.

Next, the problem:

  • There’s code to handle user activation, but it doesn’t support a GET request.

Then, the workaround:

  • Create our own view that inherits from the Djoser UserViewSet to handle the incoming GET request.
  • Override the activation method to accept the uid and token parameters coming in on the URL and remove the @action decorator that only allowed HTTP POST.
  • Override the get_serializer method to insert the uid and token values into its kwargs['data'].
  • Define a urlpattern that maps a get request to our ACTIVATION_URL to our newly-created view.
  • Profit

If you don’t recall any of the above steps from the discussion, scroll back up to see my code in the My Workaround section.

But Maybe I’m Wrong!

I’m wrong a lot. Maybe you know better. I’d be happy to see a better alternative solution. I’d also love to have a better understanding of the logic Djoser is using, because I admittedly just don’t get that. As a developer, I’m far more comfortable moving from APIs back towards the operating system and infrastructure services than I am moving into front end frameworks (though I do have to do that sometimes). So, if you understand how a ‘front end url’ is sent via email and then expected to somehow be intercepted by a front end that then sends a POST to the back end, please point me to some docs!

Share this:

  • Click to share on Twitter (Opens in new window)
  • Click to share on Reddit (Opens in new window)
  • Click to share on Tumblr (Opens in new window)
  • Click to share on Facebook (Opens in new window)

Contact Me

You should follow me on Twitter

Recent Posts

  • User Activation With Django and Djoser
  • Python Selenium Webdriver Notes
  • On Keeping A Journal and Journaling
  • What Geeks Could Learn From Working In Restaurants
  • What I’ve Been Up To
  • PyCon Talk Proposals: All You Need to Know And More
  • Sending Alerts With Graphite Graphs From Nagios
  • The Python User Group in Princeton (PUG-IP): 6 months in
  • The Happy Idiot
  • pyrabbit Makes Testing and Managing RabbitMQ Easy

Categories

  • Apple
  • Big Ideas
  • Books
  • CodeKata
  • Database
  • Django
  • Freelancing
  • Hacks
  • journaling
  • Leadership
  • Linux
  • LinuxLaboratory
  • Loghetti
  • Me stuff
  • Other Cool Blogs
  • PHP
  • Productivity
  • Python
  • PyTPMOTW
  • Ruby
  • Scripting
  • Sysadmin
  • Technology
  • Testing
  • Uncategorized
  • Web Services
  • Woodworking

Archives

  • May 2021
  • December 2020
  • January 2014
  • September 2012
  • August 2012
  • February 2012
  • November 2011
  • October 2011
  • June 2011
  • April 2011
  • February 2011
  • January 2011
  • December 2010
  • November 2010
  • September 2010
  • July 2010
  • June 2010
  • May 2010
  • April 2010
  • March 2010
  • February 2010
  • January 2010
  • December 2009
  • November 2009
  • October 2009
  • September 2009
  • August 2009
  • July 2009
  • June 2009
  • May 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • September 2008
  • August 2008
  • July 2008
  • June 2008
  • May 2008
  • April 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007
  • February 2007
  • January 2007
  • December 2006
  • November 2006
  • September 2006
  • August 2006
  • July 2006
  • June 2006
  • April 2006
  • March 2006
  • February 2006
  • January 2006
  • December 2005
  • November 2005
  • October 2005
  • September 2005
  • August 2005
  • July 2005
  • June 2005
  • May 2005
  • April 2005
  • March 2005
  • February 2005
  • January 2005
  • December 2004
  • November 2004
  • October 2004
  • September 2004
  • August 2004
© 2023 Musings of an Anonymous Geek | Powered by Minimalist Blog WordPress Theme