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

class django_otp.views.LoginView(**kwargs)[source]

This is a replacement for django.contrib.auth.views.LoginView 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.

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.LoginView simply by passing it in the authentication_form keyword parameter:

from django.contrib.auth.views import LoginView
from django_otp.forms import OTPAuthenticationForm

urlpatterns = [
    url(r'^accounts/login/$', LoginView.as_view(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.

Error messages can be customized in subclasses; see OTPAuthenticationFormMixin.

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/admin111/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(request=None, *args, **kwargs)[source]

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

Django has a mechanism for Overriding the default admin site.

Note

If you switch to OTPAdminSite before setting up your first device, you’ll find yourself with a bit of a chicken-egg problem. Remember that you can always use the addstatictoken management command to bootstrap yourself in.

As a convenience, OTPAdminSite will override the admin login template. The template is a bit of a moving target, so this may get broken by new Django versions. Users will probably have a better and more consistent experience if you send them through your own login UI instead.

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; for an example of using this in a login view, see the source for django_otp.views.LoginView.

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).

Error messages can be customized in subclasses; see OTPAuthenticationFormMixin.

Parameters:
  • user (User) – An authenticated user.

  • request (HttpRequest) – The current request.

Custom Forms

Most of the functionality of OTPAuthenticationForm and OTPTokenForm is implemented in a mixin class:

class django_otp.forms.OTPAuthenticationFormMixin[source]

Shared functionality for AuthenticationForm subclasses that wish to handle OTP tokens. Subclasses must do the following in order to use this:

  1. Define three additional form fields:

    otp_device = forms.CharField(required=False, widget=forms.Select)
    otp_token = forms.CharField(required=False, widget=forms.TextInput(attrs={'autocomplete': 'off'}))
    otp_challenge = forms.CharField(required=False)
    
    • otp_device will be a select widget with all of the user’s devices listed. Until the user has entered a valid username and password, this will be empty and may be omitted.

    • otp_token is where the user will enter their token.

    • otp_challenge is a placeholder field that captures an alternate submit button of the same name.

  2. Override clean() to call clean_otp() after invoking the inherited clean(). See OTPAuthenticationForm for an example.

  3. See OTPAuthenticationForm for information about writing a login template for this form. The file django_otp/templates/otp/admin/login.html is also a useful example.

You will most likely be able to use OTPAuthenticationForm, OTPAdminAuthenticationForm, or OTPTokenForm directly. If these do not suit your needs–for instance if your primary authentication is not by password–they should serve as useful examples.

This mixin defines some error messages in OTPAuthenticationFormMixin.otp_error_messages, which can be overridden in subclasses (refer to the source for message codes). For example:

class CustomAuthForm(OTPAuthenticationFormMixin, AuthenticationForm):
    otp_error_messages = dict(OTPAuthenticationFormMixin.otp_error_messages,
        token_required=_('Please enter your authentication code.'),
        invalid_token=_('Incorrect authentication code. Please try again.'),
    )

The Low-Level API

More customized integrations can use these APIs to manage the verification process directly.

Warning

Verifying OTP tokens should always take place inside of a transaction. If you’re loading the devices yourself, be sure to use select_for_update() to prevent concurrent access. Relevant APIs below have a for_verify parameter for this purpose.

django_otp.devices_for_user(user, confirmed=True, for_verify=False)[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 (bool) – 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.

  • for_verify (bool) – If True, we’ll load the devices with select_for_update() to prevent concurrent verifications from succeeding. In which case, this must be called inside a transaction.

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.verify_token(user, device_id, token)[source]

Attempts to verify a token against a specific device, identified by persistent_id.

This wraps the verification process in a transaction to ensure that things like throttling polices are properly enforced.

Parameters:
  • user (User) – The user supplying the token.

  • device_id (str) – A device’s persistent_id value.

  • token (str) – An OTP token to verify.

Returns:

The device that accepted token, if any.

Return type:

Device or None

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.

Warning

This originally existed for more convenient integration with the admin site. Its use is no longer recommended and it is not guaranteed to interact well with more recent features (such as throttling). Tokens should always be verified against specific devices.

Parameters:
  • user (User) – The user supplying the token.

  • token (str) – 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 LoginView 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.

classmethod from_persistent_id(persistent_id, for_verify=False)[source]

Loads a device from its persistent id:

device == Device.from_persistent_id(device.persistent_id)
Parameters:

for_verify (bool) – If True, we’ll load the device with select_for_update() to prevent concurrent verifications from succeeding. In which case, this must be called inside a transaction.

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 database.

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.

generate_is_allowed()[source]

Checks whether it is permissible to call generate_challenge(). If it is allowed, returns (True, None). Otherwise returns (False, data_dict), where data_dict contains extra information, defined by the implementation.

This method can be used to implement throttling of token generation for interactive devices. Client code should check this method before calling generate_challenge() and report problems 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

property persistent_id

A stable device identifier for forms and APIs.

verify_is_allowed()[source]

Checks whether it is permissible to call verify_token(). If it is allowed, returns (True, None). Otherwise returns (False, data_dict), where data_dict contains extra information, defined by the implementation.

This method can be used to implement throttling or locking, for example. Client code should check this method before calling verify_token() and report problems to the user.

To report specific problems, the data dictionary can return include a 'reason' member with a value from the constants in VerifyNotAllowed. Otherwise, an 'error_message' member should be provided with an error message.

verify_token() should also call this method and return False if verification is not allowed.

Return type:

(bool, dict or None)

verify_token(token)[source]

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

Parameters:

token (str) – The OTP token provided by the user.

Return type:

bool

class django_otp.models.DeviceManager(*args, **kwargs)[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.

class django_otp.models.GenerateNotAllowed(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Constants that may be returned in the reason member of the extra information dictionary returned by generate_is_allowed()

COOLDOWN_DURATION_PENDING

Indicates that a token was generated recently and we’re waiting for the cooldown period to expire.

class django_otp.models.VerifyNotAllowed(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]

Constants that may be returned in the reason member of the extra information dictionary returned by verify_is_allowed()

N_FAILED_ATTEMPTS

Indicates that verification is disallowed because of n successive failed attempts. The data dictionary should include the value of n in member failure_count

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.