Authentication without password using XMPP on a Django website

This article describes the authentication with XMPP on a Django powered website.

Authentication without password

When you authenticate on a website, the domain validate your identity before letting you access confidential information. They are several ways perform this validation and the use of passwords is the most popular. Another method is the use of a token generator i.e. a small device that generate a secret passphrase that you copy on a website. Today I will present you another authentication method without password using XMPP.

XMPP authentication

XMPP has a nice authentication mechanism. It is normalized in the XMPP extension XEP-0070. It may be used on website. There are 4 steps.

  1. The user visits its favorite website and go to the login section.
  2. The user enter its jid (XMPP address) in a form and click on a button to authenticate.
  3. The website send a XMPP request to the user asking if he wants to login on the website. The request display also a code that must be identical on the website and the XMPP client in order to validate the request.
  4. The user validate the request on its XMPP client and therefore he is login on the website.

There are plenty XMPP clients: Gajim, Salut-à-toi, Movim, Conversation, Poezio, Pidgin, Psi etc. Several of them work on mobile, on webpage or on Desktop. Therefore, it is possible to authenticate easily on a website using your smartphone, Desktop or another platform easily without password.

Note: if the client does not support the XEP-0070, there is a fallback mechanism where the user send back the validation code in a chat window. Therefore, it is possible to authenticate with all XMPP clients.

Examples

Gajim

Gajim XEP-0070

Salut à toi (Primitivus)

Primitivus XEP-0070

The following section presents the implementation of this mechanism on a Django website.

Use XMPP authentification mechanism with Django

Make it easy with HTTPAuthenticationOverXMPP

In this section, the XMPP part is managed by a component written by "Chteufleur‎". This component is easy to use. It manage the XMPP session and the web developeur just have to make a request to the component and it sends a return code:

  • 200 : User accepts the request
  • 400 : One or more mandatory parameter(s) is missing
  • 401 : User denies the request or timeout
  • 520 : Unknown error appends
  • 523 : Server is unreachable

The installation procedure is described in the Readme file of the project (https://git.kingpenguin.tk/chteufleur/HTTPAuthentificationOverXMPP).

Django files

The view manage the form fields and send the jid and validation code (transaction_id) to a module called XmppBackend. The transaction_id is generated when the form is accessed. Its value is kept in memory by using the session mechanism of Django (see section Settings.py).

Several files are needed to obtained the desired result. The following sections describes them.

Forms.py

from django import forms

class AuthForm(forms.Form):
    username = forms.CharField(max_length=100, help_text="(XMPP jid)")

HTML template

{% extends "base.html" %}

{% block content %}

{% if form.errors %}
<p>Your username is invalid. Please try again.</p>
{% endif %}

<form method="post" action="{% url 'login' %}">
   {% csrf_token %}
   <table>
       {{form.as_p}}
    </table>
    <input type="submit" value="Login" id="Login" name="login"/>
</form>
Your validation code: {{ transaction_id|linebreaks }}
<strong>{{ status_msg|linebreaks }}</strong>
{% endblock %}

Views.py

views.py reads the content of the POST and sends the result to xmpp_auth. It also handles the session and the transaction_id generation.

from django.shortcuts import render
from django.contrib.auth import login
from django.http import HttpResponse
from . import xmpp_auth
from .forms import AuthForm


def index(request):
    return render(request, 'index.html')


def xmpp_authentification(request):
    xb = xmpp_auth.XmppBackend()
    transaction_id = None
    status_msg = ""
    if request.method == 'POST':
        try:
            transaction_id = request.session.get('transaction_id')
        except KeyError:
            request.session['user_logged_in'] = False
            return render(request, 'fail.html')
        form = AuthForm(request.POST)
        # check whether it's valid:
        if form.is_valid():
            username = form.cleaned_data['username']
            user, status_code = xb.authenticate(username=username, password=None, transaction_id=transaction_id)
            if user is not None:
                login(request, user)
                # Redirect to a success page.
                request.session['user_logged_in'] = True
                return render(request, 'success.html')
            if status_code == 401:
                request.session['user_logged_in'] = False
                status_msg = "User {} refused to authenticate.".format(username)
        else:
            request.session['user_logged_in'] = False
            return render(request, 'fail.html')
    else:
        request.session['user_logged_in'] = False
        transaction_id = xb.id_generator(6)
        request.session['transaction_id'] = transaction_id
        form = AuthForm()

    return render(request, 'registration/login.html', {'form': form , 'transaction_id' : transaction_id,
                                                       'status_msg': status_msg})

xmpp_auth.py

This module makes the following request to the component:

GET /auth?jid=user%40host%2fresource;domain=example.net;method=POST;transaction_id=what_you_want;timeout=120 HTTP/1.1

The component send back a return code. In case of success, the system try to find the user in the database. If this user does not exist, it is created. The system described here is simple and the code must be adapted for more complex website (profile creation, additionnal data etc).

id_generator is called by views.py and by default, it send a code made of 8 characters (both letters and digits) but it is possible to adapt easily this behavior.

import sys
import requests
import string
import random
from django.contrib.auth.models import User


class XmppBackend(object):
    """
    Authenticate with the XMPP 00-70 XEP
    """
    def __init__(self):
        self.transaction_id = None

    def get_transaction_id(self):
        return self.transaction_id
    def set_transaction_id(self, transaction_id):
        self.transaction_id = transaction_id


    def authenticate(self, username=None, password=None, transaction_id = None):
        # Check the token and return a user.
        timeout = 300
        payload = {'jid': username, 'domain': 'agayon.be', 'method': 'POST', 'timeout': timeout,
                   'transaction_id': transaction_id}
        r = requests.get('https://auth.agayon.be/auth', params=payload)
        if r.status_code == 200:
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # Create a new user. There's no need to set a password
                user = User(username=username)
                user.is_staff = False
                user.is_superuser = False
                user.save()
            return user, r.status_code
        if r.status_code == 401:
            print("User {} refused to authenticate".format(username), file=sys.stdout)
            return None, r.status_code
        return None, r.status_code

    def id_generator(self, size=8, chars=string.ascii_letters + string.digits):
        self.transaction_id = ''.join(random.choice(chars) for _ in range(size))
        return self.transaction_id

Settings.py

The setting of the website must be adapted to your needs. In this simple example, the sessions must be enabled (it is the case by default). Our example use cached session but you can use cookies or even databases. See the excellent documentation of Django for additional information.

LOGIN_URL = '/path/to/login/'

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': 'unix:/tmp/memcached.sock',
    }
}

Links

Credits

  • The image comes from the post on Linuxfr (by Chteufleur).
  • The description of the XMPP component comes from its repository (by Chteufleur).

links

social