Passwordless

Passwordless

Making a case for Password-less in DRF(Django Rest Framework)

My New Year's Resolution of posting technical articles regularly seems to be running poorly. I'm hoping to continue writing more consistently. This is a strictly technical article, where I try to make a case for overengineering, using passwordless authentication.

The buzzword these days is undoubtedly permissionless and not passwordless. Permissionless in the Web3 space is an exciting topic/ debate, let's save that for another day. I was a part of this Hackathon where I took the initiative of implementing a passwordless authentication and was also using it in actual work stuff. So I thought I'd explore it a little deeper.

The first article that I came across made a pretty good case for passwordless. So here's the link https://www.onelogin.com/learn/passwordless-authentication. I'm just gonna be summarizing the above article and sharing a couple of thoughts that I had and also from some other sources in the coming section. So if you want you can jump directly into the part where I explore DRFPasswordless.

What is passwordless?

Passwordless architecture is a security approach that enables users to access their accounts and systems without the need for a password. Instead of relying on passwords, the passwordless architecture uses alternative authentication methods such as biometric authentication, security tokens, and mobile push notifications to verify a user's identity. Fancy definitions aside, passwordless, most often than not is an OTP-based login, that's pretty much it. So what's the fuzz about?

Why passwordless?

This approach provides several advantages over traditional password-based authentication. Passwords are often weak, reused across multiple accounts, and can be easily stolen or compromised through various methods such as phishing attacks. Passwordless authentication eliminates these issues, reducing the risk of unauthorized access and data breaches.

Passwordless architecture can also improve user experience and convenience, as users no longer have to remember and manage multiple passwords. Additionally, passwordless authentication can provide a seamless user experience across multiple devices and platforms.

Implementing passwordless architecture requires careful consideration of security and usability factors, including the selection of appropriate authentication methods and the implementation of robust security controls. However, with proper planning and implementation, passwordless authentication can be a secure and user-friendly alternative to traditional password-based authentication.

Okay great, OTP, Passwordless or whatever, Why should I care? Well for starters you won't be needing to create those dreaded registration pages with email auth that says "Click here to activate your account 🤡" (Go ahead and click it). Users would like it, except only if they hate OTPs, in that case, the customer is not always right.

It is only after trying to implement passwordless did I realise that creating a registration endpoint was easier, One could make a case that Django being Django made it harder than usual (TWSS). But using Django was crucial as I could make a case for scalability and was flexible to spin solutions on the run with python being python, with its massive community/ support for machine learning frameworks and whatnot. Okay, what about Flask? Okay, let's be real here. FastAPI? I genuinely loved Django's ORM so ORM helps translate business logic faster. I don't see the need to sell Django, cause when Pinterest, Youtube and Instagram have been scaling well with Django.

DRFPasswordless

https://github.com/aaronn/django-rest-framework-passwordless

When I was lurking through the various Auth implementation in DRF. I came across multiple solutions that were well maintained, but what caught my eye was this repository that was not so active. So I gave it a shot. It used Django Mail service for email-based OTP and Twilio for mobile login. That's great, I imported it to my backend and did simple POCs and seemed to be satisfied. Had to create a Custom User implementation to match our requirements, and this translated to a few hours in the official Documentation and Stackoverflow.

# --------------------------------------------------------------------------
# Models.py
# --------------------------------------------------------------------------
from django.db import models
from .managers import CustomUserManager
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):
    username = models.TextField(unique=False)
    email = models.EmailField(_("email address"), unique=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

# --------------------------------------------------------------------------
# Manager.py
# --------------------------------------------------------------------------
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _

class CustomUserManager(BaseUserManager):
    """
    Custom user model manager where email is the unique identifiers
    for authentication instead of usernames.
    """
    def create_user(self, email, password, **extra_fields):
        """
        Create and save a user with the given email and password.
        """
        if not email:
            raise ValueError(_("The Email must be set"))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        extra_fields.setdefault("is_active", True)

        if extra_fields.get("is_staff") is not True:
            raise ValueError(_("Superuser must have is_staff=True."))
        if extra_fields.get("is_superuser") is not True:
            raise ValueError(_("Superuser must have is_superuser=True."))
        return self.create_user(email, password, **extra_fields)

[For anyone trying to implement the same] Points to be noted, Had to override the default UserManger to map python ./manage.py createsuperuser to the custom user and Map the auth User.

# --------------------------------------------------------------------------
# Manager.py
# --------------------------------------------------------------------------

# Stuff before

AUTH_USER_MODEL = "backend.CustomUser"

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

REST_FRAMEWORK = {
     'DEFAULT_AUTHENTICATION_CLASSES': (
               'rest_framework.authentication.TokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES':(
                'rest_framework.permissions.IsAuthenticated',
    ),
}

PASSWORDLESS_AUTH = {
   'PASSWORDLESS_AUTH_TYPES': ['EMAIL'],
   'PASSWORDLESS_EMAIL_NOREPLY_ADDRESS': 'noreply@example.com',
}

if DEBUG:
    # Sends mail in the console in DEBUG MODE
    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
else:
    # Sends mail in production mode using SMTP server
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    EMAIL_HOST = 'smtp.gmail.com'
    EMAIL_USE_TLS = True
    EMAIL_PORT = 587
    EMAIL_HOST_USER = ''
    EMAIL_HOST_PASSWORD = ''

# Stuff after

Before, trying DRFPasswordless, I did try Auth0 and Oauth, but this particular requirement kept me on hold. Login for a custom domain, only. Plus I was hesitant to mess with LDAP. This meant hours of POCs and not only that I felt this was a crucial feature to be included so I decided to flex a PR/ Issue, and newsflash this repo isn't that active.

So this is when I decided to overengineer a solution. So I manually added the service and wrote a custom validator(Thank you MIT©). Honestly, this was the cleanest way that I could up with to implement a custom validator.

@deconstructible
class PSEmailValidator(EmailValidator):
    ''' Custom email validator for the domain xyz '''
    def validate_domain_part(self, domain_part):
        # can be replaced by 
        # return domain_part in ['xyz.com', 'abc.com']
        return domain_part == 'xyz.com'

    def __eq__(self, other):
        return isinstance(other, PSEmailValidator) and super().__eq__(other)

class EmailAuthSerializer(AbstractBaseAliasAuthenticationSerializer):
    @property
    def alias_type(self):
        return 'email'

    email = serializers.EmailField(validators=[PSEmailValidator()])

So yes, this is how I set up Passwordless in DRF with custom validators.

One for the geeks, Django Auth

Django's authentication system includes several built-in views and forms that can be used to implement user authentication and registration. These views and forms provide a secure way to handle user input and prevent common security vulnerabilities such as cross-site scripting (XSS) and cross-site request forgery (CSRF).

Django also includes support for pluggable authentication backends, which allows developers to customize the authentication process and use alternative authentication methods such as OAuth or LDAP. This provides flexibility and enables developers to implement authentication systems that meet the specific needs of their applications. This is where the passwordless comes in.

Login System In Python Django - Python Guides

More to read here

Final Thoughts

If you want a head-start on passwordless with DRF you could use my repo as a starter template. So how is this over-engineering, if you have used Django before you would've realised the default auth with Registration endpoints and views with CRSF already exists 😅 and had to circumvent all that and maintain the core Django structure (so that I get the fancy admin panel stuff duh) that was a hassle.

Hoping to come up with a super quick article on handling multiple login sessions in DRF and weird chatGPT stuff soon!