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.

The user visits its favorite website and go to the login section. The user enter its jid (XMPP address) in a form and click on a button to authenticate. 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. 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

Salut à toi (Primitivus)

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