10
votes

I'm using Django 2.0.10 with rest framework, rest-auth and allauth, with a React front end. Rest-auth provides login and logout functionality using JWT token authentication, but I can't work out how to allow the user to request a resend of the verification email.

I want a user to be able to log in and press a button saying "Resend confirmation email". If for example they accidentally deleted the email, they need to be able to request another.

I've seen posts suggesting that you can use send_email_confirmation from allauth, but this expects a CSRF token which would be generated by a template. I tried following the docs to excempt from csrf, but it doesn't make any different. I also tried setting authentication_classes = () as suggested here. Here's my code:

settings:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

urls.py

from users.views import EmailConfirmation

urlpatterns = [
...
url(r'^/sendconfirmationemail/', EmailConfirmation.as_view(), name='send-email-confirmation')
]

views.py

from rest_framework.views import APIView
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator

class EmailConfirmation(APIView):
    @method_decorator(csrf_exempt)
    authentication_classes = ()

    def post(self):
        send_email_confirmation(user=self.request.user)

When I post to the endpoint '/api/v1/rest-auth/sendconfirmationemail/', I get an error Forbidden:

<p>You are seeing this message because this site requires a CSRF cookie when submitting forms. This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties.</p>
<p>If you have configured your browser to disable cookies, please re-enable them, at least for this site, or for &#39;same-origin&#39; requests.</p>

Edit: I have also tried to add the CSRF token to my request following tutorials like this one. But I have the same problem. Here's what I've tried:

function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = cookies[i].trim();
            //var cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

And then I construct my request to use fetch API like this:

curl 'http://localhost:3000/api/v1/rest-auth/sendconfirmationemail/' -H 'Authorization: Token 55c8da5de68b657cf9dafd820a7f02f997fa3d64' -H 'Origin: http://localhost:3000' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36' -H 'Content-Type: text/plain;charset=UTF-8' -H 'Accept: */*' -H 'Referer: http://localhost:3000/account' -H 'Connection: keep-alive' -H 'X-CSRFToken: YOHB6RXqpZMFIXKT31T9tAjaUsH0w3eIjaaCvbgAqmP64SWeVNd3Sz3g8nZUEmVS' --data-binary 'csrfmiddlewaretoken=YOHB6RXqpZMFIXKT31T9tAjaUsH0w3eIjaaCvbgAqmP64SWeVNd3Sz3g8nZUEmVS' --compressed

When I look at working examples from Django templates, I see that the csrfmiddlewaretoken value sent with the form data is not the same as the X-CSRFToken sent in the header - I think the value supplied by Django templates is salted and this might make a difference. But no instructions I can find tell me how to get the right value? Or do I have my fetch request in the wrong form somehow?

If I use this form in my React page:

<form action="api/v1/sendconfirmationemail" method="post">
<Input type="hidden" name="csrfmiddlewaretoken"  value={this.getCookie('csrftoken')} />
<button type="submit">Send</button>
</form>

When I submit it, I get an error "Method Not Allowed (POST): /api/v1/sendconfirmationemail". The cURL from this request is:

curl 'http://localhost:3000/api/v1/sendconfirmationemail' -H 'Cookie: csrftoken=YOHB6RXqpZMFIXKT31T9tAjaUsH0w3eIjaaCvbgAqmP64SWeVNd3Sz3g8nZUEmVS; sessionid=uslpdgd5npa6wyk2oqpwkhj79xaen7nw' -H 'Origin: http://localhost:3000' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-GB,en-US;q=0.9,en;q=0.8' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Cache-Control: max-age=0' -H 'Referer: http://localhost:3000/account' -H 'Connection: keep-alive' --data 'csrfmiddlewaretoken=YOHB6RXqpZMFIXKT31T9tAjaUsH0w3eIjaaCvbgAqmP64SWeVNd3Sz3g8nZUEmVS' --compressed

Any idea how I can request an email resend from the React frontend?

1
Possible duplicate question stackoverflow.com/questions/41709347/…Thomas Myers
I don't think this is the same issue as I am using token authentication. That question uses oauth2_provider.ext.rest_framework.OAuth2Authentication.Little Brain
Either case you are not calling the method_decator on a method. It should be on top of the method. docs.djangoproject.com/en/2.1/topics/class-based-views/intro/…Thomas Myers
I tried putting @method_decorator(csrf_exempt) def post(self, request): this also did not work.Little Brain
The more I think about it, you need to have csrf_token for login, since you are using react you probably need to pass the csrf_token to the header on your own. Here is the documents on the Jquery code that will pass the cookie docs.djangoproject.com/en/2.1/ref/csrf/#ajaxThomas Myers

1 Answers

11
votes

I think part of my trouble was that the server seems to show the Forbidden (CSRF cookie not set.) error if the fetch request has the incorrect URL and is therefore not pointing at a valid rest-framework URL. This had me chasing after CSRF issues instead of double checking my URL.

I hope this will help somebody else. Here's my working code:

users/views.py

from allauth.account.signals import email_confirmed
from django.dispatch import receiver
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated

from allauth.account.utils import send_email_confirmation
from rest_framework.views import APIView

from . import models
from . import serializers

from rest_framework.authentication import TokenAuthentication
from rest_framework import status
from rest_framework.response import Response

class UserListView(generics.ListCreateAPIView):
    queryset = models.CustomUser.objects.all()
    serializer_class = serializers.UserSerializer
    authentication_classes = (TokenAuthentication,)

# when the email is confirmed, set a field on the user
# so the UI can check whether to show the "Resend confirmation email" button
@receiver(email_confirmed)
def email_confirmed_(request, email_address, **kwargs):
    user = email_address.user
    user.email_verified = True

    user.save()

# request a new confirmation email
class EmailConfirmation(APIView):
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        if request.user.email_verified:
            return Response({'message': 'Email already verified'}, status=status.HTTP_201_CREATED)

        send_email_confirmation(request, request.user)
        return Response({'message': 'Email confirmation sent'}, status=status.HTTP_201_CREATED)

api/urls.py

from users.views import EmailConfirmation

urlpatterns = [
...
    path('sendconfirmationemail/', EmailConfirmation.as_view(), name='send-email-confirmation')
]

I'm sending a POST request using JavaScript fetch(), with the authentication token in the header like this:

-H 'Authorization: Token 98254e6004e4a28b9d8cf61e7a7a9ee2fc61009a'