Fingerprint-based authentication and Authorization in Python(Django) web applications - Second part
Tags: django, webauthn, passwordless, fingerprint
Published: 2021-06-08 07:13:30

This is the continuation of [Fingerprint-based authentication and Authorization in Python(Django) web applications](https://dev.to/sirneij/fingerprint-based-authentication-and-authorization-in-python-django-web-applications-2c6l). 

#### The full code for this article can be accessed on  {% github Sirneij/django_mfa2_example %} and is live [here](https://django-mfa2-example.herokuapp.com/). 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:

```python
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"})
```
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:
```html
...
<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>
...
```
It is just a basic form with two main input fields and one submit button. These lines:
```python
username = request.POST.get('username').replace('/', '')
display_name = request.POST.get('display-name')
```
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:
```python
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
```
Note that these steps aside:
```python
if User.objects.filter(username=username).exists():
            error = 'Student already exists.'
            return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
```
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:
```python
return redirect(reverse('start_fido2'))
```
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](https://www.w3.org/TR/webauthn-2/#sctn-conforming-authenticators) 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:

```python
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')
```
This should be familiar. The real usage of `django-mfa2` comes in these lines:
```python
...
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)
...
```
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:
```python
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'))
```
to authenticate the user. This function serves as the callback which was icluded in part one of this series in our `settings.py` file:
```python
...
MFA_LOGIN_CALLBACK="accounts.views.login_user_in"            # A function that should be called by username to login the user in session
...
```

We are almost done! Include these views in our `accounts/urls.py` file:
```python
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'),
]
```
Note that `index` is included in the [complete code on github](https://github.com/Sirneij/django_mfa2_example). 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:
```html
{% extends "base.html" %}
```
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:
```html
<head>
...
{% block head %} {% endblock %} {% block css %} {% endblock css %}
...
  </head>
```

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 Sirneij/django_mfa2_example %}.

See ya in the [next part](https://dev.to/sirneij/fingerprint-based-authentication-and-authorization-in-python-django-web-applications-hacking-your-way-out-part-3-2f3e) where we tinker with the `django-mfa` source code a bit.

## References
  - [WebAuthn authenticators](https://www.w3.org/TR/webauthn-2/#sctn-conforming-authenticators) by <cite>W3C</cite>
  - [Django-mfa2 documentation](https://github.com/mkalioby/django-mfa2)