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 bothOTPAuthenticationForm
andOTPTokenForm
. This is a good view forOTP_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. If the verification fails, the
otp_verification_failed
signal is emitted. 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:
username
is inherited fromAuthenticationForm
.password
is inherited fromAuthenticationForm
.otp_device
uses aSelect
to allow the user to choose one of their registered devices. It will be empty as long asform.get_user()
isNone
and should generally be omitted from the template in that case.otp_token
is the field for entering an OTP token. It should always be included.
In addition, if
form.get_user()
is notNone
, the template should include an additional submit button namedotp_challenge
. Pressing this button whenotp_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
, andotp_token
fields should be visible. Validation ofusername
andpassword
is the same as forAuthenticationForm
. 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 authenticatedUser
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 theotp_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 theValidationError
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 ifis_staff
isFalse
.- 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 asOTPAuthenticationForm
.
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 fordjango_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.
Signals¶
- django_otp.forms.otp_verification_failed¶
This signal is sent when an OTP verification attempt fails. The source of the signal is
OTPAuthenticationFormMixin
, therefore it will generally be emitted only if using the providedOTPAuthenticationForm
orOTPTokenForm
. The signal provides the following arguments:sender
The class of the form that attempted the verification.
user
The user that attempted the verification.
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: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.
Override
clean()
to callclean_otp()
after invoking the inheritedclean()
. SeeOTPAuthenticationForm
for an example.See
OTPAuthenticationForm
for information about writing a login template for this form. The filedjango_otp/templates/otp/admin/login.html
is also a useful example.
You will most likely be able to use
OTPAuthenticationForm
,OTPAdminAuthenticationForm
, orOTPTokenForm
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 withselect_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.
- 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.
- 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 anotp_device
atribute. If you use Django’sLoginView
view with the django-otp authentication forms, then you won’t need to call this.- Parameters:
request (
HttpRequest
) – The HTTP requestdevice (
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 toFalse
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¶
- 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 withselect_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 returnNone
if this device is not interactive.- Return type:
string or
None
- Raises:
Any
Exception
is permitted. Callers should trapException
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)
, wheredata_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
ifgenerate_challenge()
has been overridden, but subclasses are welcome to provide smarter implementations.- Return type:
- 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)
, wheredata_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 inVerifyNotAllowed
. 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
)
- class django_otp.models.DeviceManager(*args, **kwargs)[source]¶
The
Manager
object installed asDevice.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=<not given>, *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 bygenerate_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=<not given>, *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 byverify_is_allowed()
- N_FAILED_ATTEMPTS¶
Indicates that verification is disallowed because of
n
successive failed attempts. The data dictionary should include the value ofn
in memberfailure_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 toOTP_LOGIN_URL
.- Parameters:
if_configured (bool) – If
True
, an authenticated user with no confirmed OTP devices will be allowed. Default isFalse
.
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.