Django: how to verify email using otp in django?

I have a question about django authentication that i have been thinking about a way to implement it for days now, is it possible to for a user, before they create an account they pass throught these steps:

  1. Enter only thier email firstly
  2. I send OTP to thier email
  3. They verify thier email using the OTP
  4. I then ask for thier username, password 1, password 2
  5. Then save all these data to the database

Please anybody know if something like this is possible using django restframework, i would really appreciate an answer.

Yes it’s possible, either using DRF or regular Django.

1 Like

please what is the best way to go about it. Do i need to save the email first in the database?
i have no clue about what to do? please how can i implement this

If I had to do this, I’d probably create two views.

View #1 -
Contains a form for the person’s email address
Accepts the form and builds the User object. (Setting all other fields to blank, null, or some reasonable defaults as appropriate.)
Builds and sends the email to the provided address, containing the link for view #2.
Saves the “OTP” for later reference.

View #2 - Accessible 1 time from the link provided
Contains the form for the rest of the personal information required (name, password, company, phone number, whatever)
Updates the User object with the supplied data
Invalidates the OTP used to access that page.

Note that fundamentally, it doesn’t matter whether you’re using Django or DRF for this - what’s going to happen in the views should pretty much be the same either way.

2 Likes

I followed your steps and so far this is whati have done also the issue that i am facing.

Firsly, i created a serilaizer to register and verify the email
serailizer.py

class VerifyAccountSerializer(serializers.Serializer):
    email = serializers.EmailField()
    otp = serializers.CharField()


class EmailSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['email']
    def create(self, validated_data):
        user = User.objects.create(
            username="Username",
            email=validated_data['email'],
        )
        user.save()
        return user

Then i created a view to register the email and send a 'verify your email ’ message

class VerifyEmail(generics.CreateAPIView):
    queryset = User.objects.all()
    permission_classes = (AllowAny,)
    serializer_class = EmailSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()
        send_otp_via_email(instance.email)
        headers = self.get_success_headers(serializer.data)
        return Response({
            'status':201,
            'message':'Verify Your Email to continue',
            'data': serializer.data,
            'headers':headers
        })

NOTE: the send otp via email is another function that i created to send the email.

this is also the view to verify the email using an otp


class VerifyOTP(APIView):
    def post(self, request):
        try:
            data = request.data
            serializer = VerifyAccountSerializer(data=data)
            
            if serializer.is_valid():
                email = serializer.data['email']
                otp = serializer.data['otp']
                
                user = User.objects.filter(email=email)
                if not user.exists():
                    return Response({
                        'status':400,
                        'message':'Email not found',
                        'data': "invalid email",
                    })
                
                if not user[0].otp == otp:
                    return Response({
                        'status':400,
                        'message':'Invalid Otp',
                        'data': "invalid otp",
                    })
                user = user.first()
                user.is_verified = True
                user.active = True
                user.save()
                
                return Response({
                    'status':200,
                    'message':'Account Verified Successfully.',
                    'data': {},
                })
            return Response({
                    'status':500,
                    'message':'Something went wrong.',
                    'data': serializer.errors,
                })
            
            
        except Exception as e:
            print(e)

So this verifies the email.

The next thing i am finding it difficult to do is, how to then retrieve this user from the database, collect the rest of the information and complete the registration.

This is what i have done.

class RegisterAccountView(generics.RetrieveUpdateAPIView):
    queryset = User.objects.all()
    permission_classes = (AllowAny,)
    serializer_class = RegisterSerializer

    def get_object(self, request, *args, **kwargs):
        user_id = self.kwargs['user_id']
        user = User.objects.get(id=user_id)
        # I don't know how to go ahead and get the email from the db
        # and also complete the registration
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()
        
        headers = self.get_success_headers(serializer.data)
        return Response({
            'status':201,
            'message':'Account Created Successfully',
            'data': serializer.data,
            'headers':headers
        })

i don’t know if i should overide the create() or continue with the get_object().
Please how do i then retrieve the user using the email they have registered then complete the registration process.

Any help would be really appreciated.

Look at how the Django forgot / reset password functionality works. You can use that as the basis for this same flow of logic. Note how they create the OTP as a token that is sent as part of the url in an email.

If you read the source for those views, it should give you a pretty solid idea of the pattern to use for this.

Hello, i’ve been trying to follow the django reset password logic for days, trying to implement the feature above.
I have tried alot of steps relating to the password forgot logic, but all of them seems not to be working, please i would really need an assistance.

This is what i have done so far

class CompleteRegisterView(generics.UpdateAPIView):
    permission_classes = (AllowAny,)
    serializer_class = CompleteRegisterSerializer
    
    def get_object(self , *args, **kwargs):
        user_id = self.kwargs['user_id']
        user = User.objects.get(id=user_id)

        if user.email and user.is_verified == True:
            serializer = self.get_serializer(data=self.request.data)
            serializer.is_valid(raise_exception=True)
            serializer.email = user.email
            instance = serializer.save()
            return Response({
                'status':201,
                'message':'Account Created Successfully, Login now',
                'data': serializer.data,
            })
        else:
            return Response({
                'status':404,
                'message':'Email not found or verified.',
                'data': {},
                'headers':{}
            })

urls.py

    path('register/<user_id>/', views.CompleteRegisterView.as_view()),

Error

AttributeError at /api/register-2/1/
'Response' object has no attribute 'pk'

Unfortunately, I can’t help you directly because I don’t use DRF enough for specific examples. About the best I might be able to do is help you understand how the existing password reset logic works.

okay please, that would be really helpful.

So what part of it are you having difficulties with?

I am having difficulties retrieving the user using thier email. What is did is get the user using thier id and the checked if the user.is_verified == True but this does not work.

user = User.objects.get(id=id)
if user.email and user.is_verified == True:
    # they can then fill in all the other information needed for creating an account

i have tried doing this using this code

class CompleteRegisterView(generics.UpdateAPIView):
    permission_classes = (AllowAny,)
    serializer_class = CompleteRegisterSerializer
    
    def get_object(self , *args, **kwargs):
        user_id = self.kwargs['user_id']
        user = User.objects.get(id=user_id)

        if user.email and user.is_verified == True:
            serializer = self.get_serializer(data=self.request.data)
            serializer.is_valid(raise_exception=True)
            serializer.email = user.email
            instance = serializer.save()
            return Response({
                'status':201,
                'message':'Account Created Successfully, Login now',
                'data': serializer.data,
            })

it doesn’t seem to be woking.

Is email a field in your User model?

If so, how would you write a query to retrieve an instance of User matching that field?

i would do it this way

email = #get email from somewhere
user = User.objects.get(email=email)

I just realized I misunderstood your question.

Taking a step back then, the password email contains a url. That url contains two segments generated by the view, a url-encoded version of the primary key of the user (uidb64), and the token used to validate the request.

That’s what I’m suggesting you reimplement in your system. Construct the email to be sent to contain the two parts of the url - both the pk for User and the token.

Thanks, I want to express my gratitude for the solution you provided to my problem as well. your guidance and expertise were incredible helpful in resolving the issue.