Authentication and Authorization

This section describes the process for verifying users against their registered OTP devices as well as limiting access based on this verification.

Authenticating Users

Soliciting an OTP token from a user is more complicated than soliciting a password. For one thing, each user may have any number of OTP devices registered to their account and the token itself won’t tell us which one is intended. And, of course, we won’t even know which devices we should check until after we’ve identified the user based on their username and password. Complicating this further is the fact some plugins are interactive, in which case verifying the user is at least a two-step process.

Verifying a user can happen in one or two stages. One option is to require an OTP up front along with a password. Alternatively, we can accept single-factor authentication initially, but allow (or require) the user to provide a second factor later on. The following sections begin with the simpler strategies and proceed to the lower-level APIs that will allow you to implement more complex policies.

The Easy Way

django_otp.views.login(request, **kwargs)[source]

This is a replacement for django.contrib.auth.views.login() that requires two-factor authentication. It’s slightly clever: if the user is already authenticated but not verified, it will only ask the user for their OTP token. If the user is anonymous or is already verified by an OTP device, it will use the full username/password/token form. In order to use this, you must supply a template that is compatible with both OTPAuthenticationForm and OTPTokenForm. This is a good view for OTP_LOGIN_URL.

Parameters are the same as login() except that this view always overrides authentication_form.

The Authentication Form

Django provides some high-level APIs to make it easy to authenticate users. If you’re accustomed to using Django’s built-in login view, this section will show you how to turn it into a two-factor login view.

In Django, user authentication actually takes place not in a view, but in an AuthenticationForm or a subclass. If you’re using Django’s built-in login view, you’re already using the default AuthenticationForm. This form performs authentication as part of its validation; validation only succeeds if the supplied credentials pass django.contrib.auth.authenticate().

If you want to require two-factor authentication in the default login view, the easiest way is to use django_otp.forms.OTPAuthenticationForm instead. This form includes additional fields and behavior to solicit an OTP token from the user and verify it against their registered devices. This form’s validation only succeeds if it is able to both authenticate the user with the username and password and also verify them with an OTP token. The form can be used with django.contrib.auth.views.login() simply by passing it in the authentication_form keyword parameter:

from django_otp.forms import OTPAuthenticationForm

urlpatterns = patterns('django.contrib.auth.views',
    url(r'^accounts/login/$', 'login', kwargs={'authentication_form': OTPAuthenticationForm}),
)
class django_otp.forms.OTPAuthenticationForm(request=None, *args, **kwargs)[source]

This form provides the one-stop OTP authentication solution. It should only be used when two-factor authentication is required: it does not have an OTP-optional mode. The form has four fields:

  1. username is inherited from AuthenticationForm.
  2. password is inherited from AuthenticationForm.
  3. otp_device uses a Select to allow the user to choose one of their registered devices. It will be empty as long as form.get_user() is None and should generally be omitted from the template in that case.
  4. otp_token is the field for entering an OTP token. It should always be included.

In addition, if form.get_user() is not None, the template should include an additional submit button named otp_challenge. Pressing this button when otp_device is set to an interactive device will cause us to generate a challenge value for the user. Pressing the challenge button with a non-interactive device selected has no effect.

The intended behavior of the form is as follows:

  • Initially the username, password, and otp_token fields should be visible. Validation of username and password is the same as for AuthenticationForm. If we are able to authenticate the user based on username and password, then one of two things happens:

    • If the user submitted an OTP token, we will enumerate all of the user’s OTP devices, asking each one to verify it in turn. If one of them succeeds, then authentication is fully successful and the user is logged in.
    • If the user did not submit an OTP token or none of user’s devices accepted it, then a ValidationError is raised.
  • In either case, as long as the user is authenticated by their password, form.get_user() will return the authenticated User object. From here on, this documentation assumes that username/password authentication succeeds on all subsequent submissions. If validation was not successful, then the form will be displayed again and this time the template should be sure to include the (now populated) otp_device field as well as the otp_challenge submit button.

  • The user will then have to choose a specific device to authenticate against (or accept the default). If they press the otp_challenge button, we will ask that device to generate a challenge. The device will return a message for the user, which will be incorporated into the ValidationError message.

  • If the user presses any other submit button, we will authenticate the username and password as always and then verify the OTP token against the chosen device. When that succeeds, authentication and verification are successful and the user is logged in.

Following is a sample template snippet that’s designed for OTPAuthenticationForm:

<form action="." method="POST">
    <div class="form-row"> {{ form.username.errors }}{{ form.username.label_tag }}{{ form.username }} </div>
    <div class="form-row"> {{ form.password.errors }}{{ form.password.label_tag }}{{ form.password }} </div>
    {% if form.get_user %}
    <div class="form-row"> {{ form.otp_device.errors }}{{ form.otp_device.label_tag }}{{ form.otp_device }} </div>
    {% endif %}
    <div class="form-row"> {{ form.otp_token.errors }}{{ form.otp_token.label_tag }}{{ form.otp_token }} </div>
    <div class="submit-row">
        <input type="submit" value="Log in"/>
        {% if form.get_user %}<input type="submit" name="otp_challenge" value="Get Challenge" />{% endif %}
    </div>
</form>

The Admin Site

In addition to providing OTPAuthenticationForm for your normal login views, django-otp includes an AdminSite subclass for admin integration.

class django_otp.admin.OTPAdminSite(name='otpadmin')[source]

This is an AdminSite subclass that requires two-factor authentication. Only users that can be verified by a registered OTP device will be authorized for this admin site. Unverified users will be treated as if is_staff is False.

has_permission(request)[source]

In addition to the default requirements, this only allows access to users who have been verified by a registered OTP device.

login_form

alias of OTPAdminAuthenticationForm

login_template = 'otp/admin19/login.html'

We automatically select a modified login template based on your Django version. If it doesn’t look right, your version may not be supported, in which case feel free to replace it.

name = 'otpadmin'

The default instance name of this admin site. You should instantiate this class as OTPAdminSite(OTPAdminSite.name) to make sure the admin templates render the correct URLs.

class django_otp.admin.OTPAdminAuthenticationForm(*args, **kwargs)[source]

An AdminAuthenticationForm subclass that solicits an OTP token. This has the same behavior as OTPAuthenticationForm.

See the Django AdminSite documentation for more on installing custom admin sites. If you want to copy the default admin site into an OTPAdminSite, we find that the following works well. Note that it relies on a private property, so use this at your own risk:

otp_admin_site = OTPAdminSite(OTPAdminSite.name)
for model_cls, model_admin in admin.site._registry.iteritems():
    otp_admin_site.register(model_cls, model_admin.__class__)

The Token Form

If you already have an authenticated user and you just want to ask for an OTP token to verify, you can use django_otp.forms.OTPTokenForm.

class django_otp.forms.OTPTokenForm(user, request=None, *args, **kwargs)[source]

A form that verifies an authenticated user. It looks very much like OTPAuthenticationForm, but without the username and password. The first argument must be an authenticated user; you can use this in place of AuthenticationForm by currying it:

from functools import partial

from django.contrib.auth.decoratorrs import login_required
from django.contrib.auth.views import login


@login_required
def verify(request):
    form_cls = partial(OTPTokenForm, request.user)

    return login(request, template_name='my_verify_template.html', authentication_form=form_cls)

This form will ask the user to choose one of their registered devices and enter an OTP token. Validation will succeed if the token is verified. See OTPAuthenticationForm for details on writing a compatible template (leaving out the username and password, of course).

Parameters:
  • user (User) – An authenticated user.
  • request (HttpRequest) – The current request.

The Low-Level API

django_otp.devices_for_user(user, confirmed=True)[source]

Return an iterable of all devices registered to the given user.

Returns an empty iterable for anonymous users.

Parameters:
  • user (User) – standard or custom user object.
  • confirmed – If None, all matching devices are returned. Otherwise, this can be any true or false value to limit the query to confirmed or unconfirmed devices, respectively.
Return type:

iterable

django_otp.user_has_device(user, confirmed=True)[source]

Return True if the user has at least one device.

Returns False for anonymous users.

Parameters:
  • user (User) – standard or custom user object.
  • confirmed – If None, all matching devices are considered. Otherwise, this can be any true or false value to limit the query to confirmed or unconfirmed devices, respectively.
django_otp.match_token(user, token)[source]

Attempts to verify a token on every device attached to the given user until one of them succeeds. When possible, you should prefer to verify tokens against specific devices.

Parameters:
  • user (User) – The user supplying the token.
  • token (string) – An OTP token to verify.
Returns:

The device that accepted token, if any.

Return type:

Device or None

django_otp.login(request, device)[source]

Persist the given OTP device in the current session. The device will be rejected if it does not belong to request.user.

This is called automatically any time django.contrib.auth.login() is called with a user having an otp_device atribute. If you use Django’s login() view with the django-otp authentication forms, then you won’t need to call this.

Parameters:
  • request (HttpRequest) – The HTTP request
  • device (Device) – The OTP device used to verify the user.
class django_otp.models.Device(*args, **kwargs)[source]

Abstract base model for a device attached to a user. Plugins must subclass this to define their OTP models.

Warning

OTP devices are inherently stateful. For example, verifying a token is logically a mutating operation on the device, which may involve incrementing a counter or otherwise consuming a token. A device must be committed to the database before it can be used in any way.

user

ForeignKey: Foreign key to your user model, as configured by AUTH_USER_MODEL (User by default).

name

CharField: A human-readable name to help the user identify their devices.

confirmed

BooleanField: A boolean value that tells us whether this device has been confirmed as valid. It defaults to True, but subclasses or individual deployments can force it to False if they wish to create a device and then ask the user for confirmation. As a rule, built-in APIs that enumerate devices will only include those that are confirmed.

objects

A DeviceManager.

generate_challenge()[source]

Generates a challenge value that the user will need to produce a token. This method is permitted to have side effects, such as transmitting information to the user through some other channel (email or SMS, perhaps). And, of course, some devices may need to commit the challenge to the databse.

Returns:A message to the user. This should be a string that fits comfortably in the template 'OTP Challenge: {0}'. This may return None if this device is not interactive.
Return type:string or None
Raises:Any Exception is permitted. Callers should trap Exception and report it to the user.
is_interactive()[source]

Returns True if this is an interactive device. The default implementation returns True if generate_challenge() has been overridden, but subclasses are welcome to provide smarter implementations.

Return type:bool
verify_token(token)[source]

Verifies a token. As a rule, the token should no longer be valid if this returns True.

Parameters:token (string) – The OTP token provided by the user.
Return type:bool
class django_otp.models.DeviceManager[source]

The Manager object installed as Device.objects.

devices_for_user(user, confirmed=None)[source]

Returns a queryset for all devices of this class that belong to the given user.

Parameters:
  • user (User) – The user.
  • confirmed – If None, all matching devices are returned. Otherwise, this can be any true or false value to limit the query to confirmed or unconfirmed devices, respectively.

Authorizing Users

If you design your site to always require OTP verification in order to log in, then your authorization policies don’t need to change. request.user.is_authenticated() will be effectively synonymous with request.user.is_verified(). If, on the other hand, you anticipate having both verified and unverified users on your site, you’re probably intending to limit access to some resources to verified users only. The primary tool for this is otp_required:

@django_otp.decorators.otp_required([redirect_field_name='next', login_url=None, if_configured=False])

Similar to login_required(), but requires the user to be verified. By default, this redirects users to OTP_LOGIN_URL.

Parameters:if_configured (bool) – If True, an authenticated user with no confirmed OTP devices will be allowed. Default is False.

If you need more fine-grained control over authorization decisions, you can use request.user.is_verified() to determine whether the user has been verified by an OTP device. if is_verified() is true, then request.user.otp_device will be set to the Device object that verified the user. This can be useful if you want to include the name of the verifying device in the UI.

If you want to use OTPs to establish trusted user agents (e.g. a browser that the user claims is on a private and secure computer), look at django-agent-trust and django-otp-agents.

Managing Devices

django-otp does not include any standard mechanism for managing a user’s devices outside of the admin interface. All plugins are expected to include admin integration, which should be sufficient for many sites. Some sites may want to provide users a self-service API to manage devices, but this will be very site-specific. Fortunately, managing a user’s devices is just a matter of managing Device-derived model objects, so it will be easy to implement. Be sure to note the warning about unsaved Device objects.