read

Integrating authentication is the important part for any product, and being a backend developer myself I know this is the first thing which is to be implemented, So I am going to share my knowledge of implementing authentication which is generic and can be implemented anywhere, Let suppose that your users can signup by three ways but all the three authentications must access the same profile.

1) Facebook (provides a uniqueID when the user gets authenticated.)
2) Google (provides a uniqueID when the user gets authenticated.)
3) Manual (User sets a password in-app functionality.)

I will be creating the endpoints for signUp, signIn, testAPI (to check if authentication works) and signOut. I have used Java and GoLang earlier for most of the backend related work, and while learning Django I learned that Django provides us with a User model and its authentication mechanism prebuilt, Wow!! something which was missing in the languages which I have already used, but I want to have my own User model with different features and different authentication mechanism then what Django provides. So I will be going to discuss more in detail of my custom user authentication. So let’s create a Django project with the command

django-admin startproject custom-auth
python manage.py startapp auth-app

the above command will create a new project with its settings.py file where the database credentials will be saved. Let’s create a conf file in your project’s directory and save all the database credentials in this file, then add path of this file in your settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': 'path-to-your/database-credentials.cnf',
        },
    }
}

Now our project is connected to a database. We will be using serializers package from the rest-framework library for rendering the JSON response. Now let’s create our models in our auth-app folder, Our first model will be UserAuth which is defined as:

class UserAuth(models.Model):
    ACCOUNT_TYPES = (
        (1, 'FACEBOOK'),
        (2, 'GOOGLE'),
        (3, 'MANUAL')
    )
    user=models.ForeignKey(CustomUser,
                           related_name='userAuths',
                           on_delete=models.CASCADE,
                           null=False)
    uniqueId = models.CharField(max_length=255,
                                null=False,
                                blank=False)
    type = models.IntegerField(choices=ACCOUNT_TYPES,default=3)
    
    def from_dict(self,j):
        self.__dict__.update(j)

Where User is a foreign key referencing to our User model and uniqueId is the secret key provided by Google/Facebook or it is a password in case of manual authentication. Let’s create our User model now

from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import UserManager
class CustomUser(AbstractBaseUser):
    ACTIVE = 1
    INACTIVE = 2
    REGISTERED = 3
    DEACTIVATED = 4
    WAITING_FOR_APPROVAL = 5

    STATUS_CHOICES = (
        (ACTIVE, 'ACTIVE'),
        (INACTIVE, 'INACTIVE'),
        (REGISTERED, 'REGISTERED'),
        (DEACTIVATED,'DEACTIVATED'),
        (WAITING_FOR_APPROVAL,'WAITING_FOR_APPROVAL')
    )

    name = models.CharField(max_length=255, blank=False)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    status = models.IntegerField(choices=STATUS_CHOICES, default=ACTIVE)
    email = models.EmailField(max_length=70, 
                              null=False, 
                              blank=False, 
                              unique=True)
    mobile = models.CharField(max_length=255, blank=False)
    imageUrl = models.CharField(max_length=255, blank=False)
    address = models.CharField(max_length=255, blank=True)
    lat = models.DecimalField(decimal_places=10, max_digits=20)
    lng = models.DecimalField(decimal_places=10, max_digits=20)
    follows = models.ManyToManyField('self',
                                      blank=True,
                                      symmetrical=False,
                                      related_name='followed_by')
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = [name,mobile,lat,lng]
    objects = UserManager()
    active = True
    staff = True
    admin = True


    def followers_count(self):
        return self.followed_by.all().count()

    def following_count(self):
        return self.follows.all().count()
    isFollowing = 0

    def get_full_name(self):
        return self.name


    def get_short_name(self):
        return self.name


    def __str__(self):  # __unicode__ on Python 2
        return self.name


    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True


    def has_module_perms(self, app_label):
        "Does the user have permissions to 
         view the app`app_label`?"
        # Simplest possible answer: Yes, always
        return True


    @property
    def is_staff(self):
        "Is the user a member of staff?"
        return self.staff


    @property
    def is_admin(self):
        "Is the user a admin member?"
        return self.admin


    @property
    def is_active(self):
        "Is the user active?"
        return self.active


    def from_dict(self,j):
        self.__dict__.update(j)

So above we have extended Django’s User model with our own properties yet we can use Django’s authentication now, after adding the line in settings.py

AUTH_USER_MODEL = 'auth-app.CustomUser'

After the line above when we run our project, our custom model will be used instead of Django’s user model. Let’s define our serializer for our custom-user.

class UserSerializer(serializers.ModelSerializer):
    followersCount =     serializers.ReadOnlyField(source='followers_count')
     followingCount = serializers.ReadOnlyField(source='following_count')
    class Meta:
        model = CustomUser
        fields = ('id','name','date_created','date_modified',
                 'status','email','mobile','imageUrl',
                 'oneLiner','address','lat','lng','followersCount',
                 'followingCount')
        read_only_fields = ('id','date_created', 'date_modified',
                            'email','followersCount','followingCount')

where followingCount and followersCount is non-database fields. Ok so till now we have defined our models and serializer to render JSON. Let’s define some paths and handlers now. In our urls.py of auth-app folder add these URLs.

from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import SignUp,TestApi,SignOut,SignIn

urlpatterns = {
    url(r'^api/signUp', SignUp.as_view(),name="signUp"),
    url(r'^api/testApi', TestApi.as_view(),name="testApi"),
    url(r'^api/signOut', SignOut.as_view(), name="signOut"),
    url(r'^api/signIn', SignIn.as_view(), name="signIn"),
}

urlpatterns = format_suffix_patterns(urlpatterns)

Now let’s define some handlers for our paths. In our views.py

from __future__ import unicode_literals
import json
from rest_framework import status
from rest_framework.views import APIView
from .serializers import UserSerializer

from .models import CustomUser,UserAuth

from rest_framework.response import Response
import urllib.request
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth import login, authenticate, logout
import logging
from django.contrib.auth.decorators import login_required
# Get an instance of a logger
logger = logging.getLogger(__name__)

# Create your views here.

def getAddress(lat,long):
    base = "http://maps.googleapis.com/maps/api/geocode/json?"
    params = "latlng={lat},{lon}&sensor={sen}".format(
        lat=lat,
        lon=long,
        sen='false'
    )
    url = "{base}{params}".format(base=base, params=params)
    request = urllib.request.Request(url)
    response = urllib.request.urlopen(request)
    tmp = json.loads(response.read())
    if (tmp['status'] != 'OK'):
        return ''
    return  tmp['results'][0]['formatted_address']

class SignUp(APIView):
    @transaction.atomic
    def post(self, request, format=None):
        try:
            with transaction.atomic():
                user = CustomUser()
                user.from_dict(request.data.get('user'))
                try:
                    CustomUser.objects.get(email=user.email)
                except ObjectDoesNotExist:
                    if user.address == "":
                        user.address = getAddress(user.lat, user.lng)
                    user.save()
                userFromDB = CustomUser.objects.get(email=user.email)
                userAuth = UserAuth()
                userAuth.from_dict(request.data.get('userAuth'))
                userAuth.user_id= userFromDB.id
                try:
                    userAuthFromDB = UserAuth.objects.get(user_id=userFromDB.id,type=userAuth.type)
                    userAuthFromDB.uniqueId = userAuth.uniqueId
                    userAuthFromDB.save()
                except ObjectDoesNotExist:
                    userAuth.save()
                user = authenticate(username=user.email,
                                    type=userAuth.type,
                                    password=userAuth.uniqueId)
                logging.debug("user with login %s does exists " % user)
                login(request, user)
                return Response(UserSerializer(user).data)
        except Exception as e:
            return Response({"message": repr(e)}, status.HTTP_400_BAD_REQUEST, content_type="application/json")

class TestApi(APIView):
    def get(self, request, format=None):
        try:
            user = request.user
            return Response(UserSerializer(user).data)
        except Exception as e:
            return Response({"message": "Not logged in cannot access current user"},
                         content_type="application/json")

class SignOut(APIView):
    def get(self, request, format=None):
        try:
            user = request.user
            logout(request)
            return Response({"message": "{} was sign-out successfully".format(user)},
                         content_type="application/json")
        except Exception as e:
            return Response({"message": "sign-out failed"},
                         content_type="application/json")

class SignIn(APIView):
    @transaction.atomic
    def post(self, request, format=None):
        try:
            with transaction.atomic():
                user = CustomUser()
                user.from_dict(request.data.get('user'))
                userAuth = UserAuth()
                userAuth.from_dict(request.data.get('userAuth'))
                user = authenticate(username=user.email,
                                    type=userAuth.type,
                                    password=userAuth.uniqueId)
                logging.debug("user with login %s does exists " % user)
                login(request, user)
                return Response(UserSerializer(user).data)
        except Exception as e:
            return Response({"message": "invalid credentials"}, status.HTTP_400_BAD_REQUEST, content_type="application/json")

So We have defined our endpoints and handlers but it will not work as expected because we have not defined our custom authentication method yet. Django has many authentication functions defined for its own User model like token authentication, username-password authentication, etc, but our custom user has no password attribute nor any token, we want to write our own authentication function, so let’s create one, create a file authentication.py in the auth-app folder.

from .models import CustomUser,UserAuth
import logging

# Get an instance of a logger
logger = logging.getLogger(__name__)

class MyClassBackEnd:
    def authenticate(self, username=None, password=None,type=None,):
        try:

            # Try to find a user matching your username
            currentUser = CustomUser.objects.get(email=username)
            if type == None:
                type = 3
            userAuth = UserAuth.objects.get(type=type,user=currentUser)

            #  Check the password is the reverse of the username
            if password == userAuth.uniqueId:
                # Yes? return the Django user object
                return currentUser
            else:
                # No? return None - triggers default login failed
                return None
        except CustomUser.DoesNotExist or UserAuth.DoesNotExist:
            # No user was found, return None - triggers default login failed
            return None

    # Required for your backend to work properly - unchanged in most scenarios
    def get_user(self, user_id):
        try:
            return CustomUser.objects.get(pk=user_id)
        except CustomUser.DoesNotExist:
            return None

Ok, so what we are doing above is authenticating our user based on user-auth which was received from client side and matching the uniqueID in our database. In case of not found we just return None, Now add our custom authentication in settings.py file.

AUTHENTICATION_BACKENDS = (
    'auth-app.authentication.MyClassBackEnd',
)

We have now implemented our authentication system successfully, where a user session will be saved in django-sessions when a user sign-up or sign-in and request.user will be saved in the request context which we can access anytime to fetch the logged-in user, and entry from django-sessions will be deleted.

python manage.py makemigrations
python manage.py migrate
python manage.py runserver 8080

The above commands will create tables for our models and run a server instance at 8080 port Now let’s test our end-points:
1) http://localhost:8080/api/mobile/signUp (POST)

Request:
{
   "user": {
   "name": "rahul",
   "status": 1,
   "email": "rahul.yadav@hotcocoasoftware.com",
   "mobile": "80810800102",
   "imageUrl": "s3.rahul.bucket.1.jog",
   "oneLiner": "i am awsome",
   "lat": 28.537278,
   "lng": 77.2080403
 },
 "userAuth": {
  "uniqueId": "iamrahul",
  "type": 3
 }
}
Response:
{
    "id": 5,
    "name": "rahul",
    "date_created": "2017-12-21T08:21:59.043936Z",
    "date_modified": "2017-12-21T08:21:59.043990Z",
    "status": 1,
    "email": "rahul.yadav@hotcocoasoftware.com",
    "mobile": "80810800102",
    "imageUrl": "s3.rahul.bucket.1.jog",
    "oneLiner": "i am awsome",
    "address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
    "lat": "28.5372780000",
    "lng": "77.2080403000",
    "followersCount": 0,
    "followingCount": 0,
}

2) http://localhost:8080/api/mobile/testApi (GET)

Response:
{
    "id": 5,
    "name": "rahul",
    "date_created": "2017-12-21T08:21:59.043936Z",
    "date_modified": "2017-12-21T08:21:59.043990Z",
    "status": 1,
    "email": "rahul.yadav@hotcocoasoftware.com",
    "mobile": "80810800102",
    "imageUrl": "s3.rahul.bucket.1.jog",
    "oneLiner": "i am awsome",
    "address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
    "lat": "28.5372780000",
    "lng": "77.2080403000",
    "followersCount": 0,
    "followingCount": 0,
}

3) http://localhost:8080/api/mobile/signOut (GET)

Response:
{
    "message": "rahul was sign-out successfully"
}

Now If you again hit the http://localhost:8080/api/mobile/testApi(GET), you will get response

{
    "message": "Not logged in cannot access current user"
}

4) At last lets test our signIn request http://localhost:8080/api/mobile/signIn(POST)

Request:
{
   "user": {
   "email": "rahul.yadav@hotcocoasoftware.com"
  },
 "userAuth": {
  "uniqueId": "iamrahul",
  "type": 3
 }
}
Response:
{
    "id": 5,
    "name": "rahul",
    "date_created": "2017-12-21T08:21:59.043936Z",
    "date_modified": "2017-12-21T08:21:59.043990Z",
    "status": 1,
    "email": "rahul.yadav@hotcocoasoftware.com",
    "mobile": "80810800102",
    "imageUrl": "s3.rahul.bucket.1.jog",
    "oneLiner": "i am awsome",
    "address": "46, Guru Govind Singh Rd, Begumpur, New Delhi, Delhi 110017, India",
    "lat": "28.5372780000",
    "lng": "77.2080403000",
    "followersCount": 0,
    "followingCount": 0,
}

In case of wrong type-uniqueId being sent response will be

{
    "message": "invalid credentials"
}

After success response from our sign-in request, we can again access our testApi endpoint with the response as the currently logged-in user.

Thanks for reading please suggest any edits.

NOTE: uniqueId can be saved as hash if you want, In this blog, I just want to show how we can use Django authentication on our own models.
Blog Logo

Rahul Yadav


Published

Image

Hot Cocoa Software

Blogs written by our developers.

Back to Overview