DEV Community

Cover image for Fingerprint-based authentication and Authorization in Python(Django) web applications - Second part
John Idogun
John Idogun

Posted on • Updated on

Fingerprint-based authentication and Authorization in Python(Django) web applications - Second part

This is the continuation of Fingerprint-based authentication and Authorization in Python(Django) web applications.

The full code for this article can be accessed on

GitHub logo Sirneij / django_mfa2_example

A simple fingerprint-based authentication and authorization application using django-mfa2

django_mfa2_example

Fingerprint-based authentication and authorization system in Python (Django). This can be integrated with e-voting systems and other applications that should be very secure.

A walk-through of this repository can be found on dev.to in this tutorial-like article Fingerprint-based authentication and authorization in Python(Django) web applications. This example application uses Django-mfa2 to implement a password-less fingerprint-based authentication and authorization system. It's live and can be accessed here.

Run locally

  • clone this report:
    git clone https://github.com/Sirneij/django_mfa2_example.git
    
  • create and activate virtual environment (I used pipenv but you can stick with venv, virtualenv or poetry):
    pipenv shell
    pipenv install
    
  • makemigrations and migrate:
    python manage.py makemigrations
    python manage.py migrate
    
  • optionally, createsuperuser:
    python manage.py createsuperuser
    
and is live here. In the live version, your username must start with CPE.

It is time to get into the meat of the article. Let's add some codes to our accounts/views.py file to handle registration and logging in. For registration, add this code snippet:

def register(request):
    if request.method == "POST":
        error = ''
        username = request.POST.get('username').replace('/', '')
        display_name = request.POST.get('display-name')
        if not utils.validate_username(username):
           error = 'Invalid matriculation number'
           return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
        if not utils.validate_display_name(display_name):
           error = 'Invalid display name'
           return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
        if User.objects.filter(username=username).exists():
            error = 'Student already exists.'
            return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
        else:
            u = User.objects.create(first_name = display_name, password='none', is_superuser=False, username=username,  last_name='', display_name=display_name, email='none', is_staff=False, is_active=True,date_joined=timezone.now())
            u.backend = 'django.contrib.auth.backends.ModelBackend'
            auth.login(request,u)
            return redirect(reverse('start_fido2'))
    else:
        return render(request, 'register.html', context = {'page_title': "Register"})
Enter fullscreen mode Exit fullscreen mode

This is a function-based view in django. It checks to ensure that the incoming request is a POST and then gets the username and display_name from the registration form found in accounts/templates/register.html. Some of the snippets in this .html file is:

...
<form method="POST">
      {% csrf_token %}
      <img
        class="mb-4"
        src="{% static 'images/logo.png' %}"
        alt=""
        width="72"
        height="57"
      />
      <h1 class="h3 mb-3 fw-normal">Register</h1>

      {% if error %}
      <p class="mb-3 fw-normal text-danger">{{error}}</p>
      {% endif %}

      <div class="form-floating mb-2">
        <input
          type="text"
          name="username"
          id="username"
          class="form-control"
          placeholder="username"
        />

        <label for="username">Username</label>
      </div>
      <div class="form-floating mb-2">
        <input
          type="text"
          name="display-name"
          id="display-name"
          class="form-control"
          placeholder="display-name"
        />

        <label for="display-name">Display name</label>
      </div>

      <input
        type="submit"
        value="Register"
        class="w-100 btn btn-lg btn-primary"
      />
      <p class="mt-5 mb-3 text-muted">&copy; 2021</p>
    </form>
...
Enter fullscreen mode Exit fullscreen mode

It is just a basic form with two main input fields and one submit button. These lines:

username = request.POST.get('username').replace('/', '')
display_name = request.POST.get('display-name')
Enter fullscreen mode Exit fullscreen mode

get the value of the field with username and display_name as name attribute respectively. The .replace('/', '') at the end of the username replaces any / with nothing. Which means if you type in CPE/34/3435, username will have CPE343435 without the slashes.

Then, it checks the collected values against some cleaning functions written in accounts/utils.py. Some of the snippets there is:

def validate_username(username):
    if not isinstance(username, six.string_types):
        return False

    if len(username) > USERNAME_MAX_LENGTH:
        return False

    if not username.isalnum():
        return False

    if not username.lower().startswith("cpe"):
        return False

    return True


def validate_display_name(display_name):
    if not isinstance(display_name, six.string_types):
        return False

    if len(display_name) > DISPLAY_NAME_MAX_LENGTH:
        return False

    if not display_name.replace(' ', '').isalnum():
        return False

    return True
Enter fullscreen mode Exit fullscreen mode

Note that these steps aside:

if User.objects.filter(username=username).exists():
            error = 'Student already exists.'
            return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
Enter fullscreen mode Exit fullscreen mode

ain't that necessary. They are just domain-specific for the application I used it for.

The rest lines should be familiar. What really brings up fingerprint is this line:

return redirect(reverse('start_fido2'))
Enter fullscreen mode Exit fullscreen mode

It redirects the user, having passsed all the conditions stated, to a function in django-mfa that incepts the authentication process. Notice that the user was logged in before redirecting. This is to ensure that the generated public key is linked to a particular user. If you don't want this or if you want users without any authenticator to be removed from the database and then logged out, there is a hack I used and will be shared in a later part of this series.

That is it about the registration. Now to authentication. Add this snippet to the accounts/views.py file:

def login(request):
    if request.method == "POST":
        username = request.POST.get('username').replace('/', '')
        user = User.objects.filter(username=username).first()
        err=""
        if user is not None:
            if user.is_active:
                if "mfa" in settings.INSTALLED_APPS:
                    from mfa.helpers import has_mfa
                    res =  has_mfa(request,username=username)
                    if res: return res
                    return login_user_in(request, username)
            else:
                err="This student is NOT activated yet."
        else:
            err="No student with such matriculation number exists."
        return render(request, 'login.html', {"err":err})
    else:
        return render(request, 'login.html')
Enter fullscreen mode Exit fullscreen mode

This should be familiar. The real usage of django-mfa2 comes in these lines:

...
if "mfa" in settings.INSTALLED_APPS:
   from mfa.helpers import has_mfa
   res =  has_mfa(request,username=username)
   if res: return res
   return login_user_in(request, username)
...
Enter fullscreen mode Exit fullscreen mode

It first checks to ensure mfa is installed and then verifies that the user coming in has some identity with it. Thence, the user is forwarded to login_user_in(request, username) with the following code snippet:

def login_user_in(request, username):
    user=User.objects.get(username=username)
    user.backend='django.contrib.auth.backends.ModelBackend'
    auth.login(request, user)
    if "next" in request.POST:
        return redirect(request.POST.get("next"))
    else:
        return redirect(reverse('accounts:index'))
Enter fullscreen mode Exit fullscreen mode

to authenticate the user. This function serves as the callback which was icluded in part one of this series in our settings.py file:

...
MFA_LOGIN_CALLBACK="accounts.views.login_user_in"            # A function that should be called by username to login the user in session
...
Enter fullscreen mode Exit fullscreen mode

We are almost done! Include these views in our accounts/urls.py file:

from django.urls import path
from . import views
from django.contrib.auth import views as auth_views

app_name = 'accounts'

urlpatterns = [
    path('', views.index, name='index'),
    path('login/', views.login, name='login'),
    path('register/', views.register, name='register'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
Enter fullscreen mode Exit fullscreen mode

Note that index is included in the complete code on github. You can also find all the CSS, HTML and JavaScript there.

To use django-mfa2 well, your template should have a file named mfa_auth_base.html with the contents below:

{% extends "base.html" %}
Enter fullscreen mode Exit fullscreen mode

This just extends your base.html file. For all mfa views to have the feel and look of your site, ensure you include the file above and on the head part of your base.html include:

<head>
...
{% block head %} {% endblock %} {% block css %} {% endblock css %}
...
  </head>
Enter fullscreen mode Exit fullscreen mode

Now our app is up and running! Test it out.
Please note that webauthn and fido2 only works via https protocol. Using http won't work as expected.

The complete code is on

GitHub logo Sirneij / django_mfa2_example

A simple fingerprint-based authentication and authorization application using django-mfa2

django_mfa2_example

Fingerprint-based authentication and authorization system in Python (Django). This can be integrated with e-voting systems and other applications that should be very secure.

A walk-through of this repository can be found on dev.to in this tutorial-like article Fingerprint-based authentication and authorization in Python(Django) web applications. This example application uses Django-mfa2 to implement a password-less fingerprint-based authentication and authorization system. It's live and can be accessed here.

Run locally

  • clone this report:
    git clone https://github.com/Sirneij/django_mfa2_example.git
    
  • create and activate virtual environment (I used pipenv but you can stick with venv, virtualenv or poetry):
    pipenv shell
    pipenv install
    
  • makemigrations and migrate:
    python manage.py makemigrations
    python manage.py migrate
    
  • optionally, createsuperuser:
    python manage.py createsuperuser
    

.

See ya in the next part where we tinker with the django-mfa source code a bit.

References

Discussion (0)