Overriding simple-jwt's TokenObtainPairSerializer to implement 2FA

Question:

I am currently trying to implement 2-Factor Authentication in my Django Application. The first thing I’ve done is to modify the Meta class in the UserSerializer class to add two fields enabled (indicates if 2FA is enabled for a user) and secret_key (the key to generate OTP, that is shared to the user when he enables 2FA).

To minimally modify the login flow, I’ve modified the form that is sent to generate the access tokens to include a new field "otp". The user can fill it or not, and the backend will check if the user has 2FA enabled, and if yes, if the OTP is correct.

Without 2FA, the login is simply a POST request with body {"username": usr, "password": pwd}. This has become a POST request with body {"username": usr, "password": pwd, "otp": otp}. If a user user hasn’t enabled 2FA, he can simply leave the opt field blank.

My urls.py looks like this:

path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair")

My idea is to override TokenObtainPairView to adapt to the new request. From what I’ve found, I have to change the validate method, but I don’t really have a clue as to how to do that. I probably would have to get the values of the enabled and secret_key fields of the user (based on username) to generate the OTP (if relevant) and check it against the otp field. Problem is, I don’t know how to do that and I’m getting a little bit lost in the simple-jwt implementation.

Asked By: Kins

||

Answers:

First of all, I wont provide full solution to your problem, but it might be a good start.

Create your custom LoginView:

class LoginView(TokenObtainPairView):
    serializer_class = LoginSerializer

Implement your own serializer which inherits TokenObtainPairSerializer:

class LoginSerializer(TokenObtainPairSerializer):
    def validate(self, attrs):
        # implement your logic here
        # data = super().validate(attrs)
        return data

Change urls.py

path("api/token/", LoginView.as_view(), name="token_obtain_pair")

This is how TokenObtainSerializer looks like:

class TokenObtainSerializer(serializers.Serializer):
    username_field = User.USERNAME_FIELD

    default_error_messages = {
        'no_active_account': _('No active account found with the given credentials')
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields[self.username_field] = serializers.CharField()
        self.fields['password'] = PasswordField()

    def validate(self, attrs):
        authenticate_kwargs = {
            self.username_field: attrs[self.username_field],
            'password': attrs['password'],
        }
        try:
            authenticate_kwargs['request'] = self.context['request']
        except KeyError:
            pass

        self.user = authenticate(**authenticate_kwargs)

        if not getattr(login_rule, user_eligible_for_login)(self.user):
            raise exceptions.AuthenticationFailed(
                self.error_messages['no_active_account'],
                'no_active_account',
            )

        return {}

So you could implement __init___ in your own serializer and add your otp field and implement logic you want inside validate method(you can access self.user here and check if he has 2FA enabled or not).

Answered By: token