1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
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
~~~~~~~~~~~~
.. autoclass:: django_otp.views.LoginView
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
:class:`~django.contrib.auth.forms.AuthenticationForm` or a subclass. If you're
using Django's :class:`built-in login view
<django.contrib.auth.views.LoginView>`, you're already using the default
AuthenticationForm. This form performs authentication as part of its
validation; validation only succeeds if the supplied credentials pass
:func:`django.contrib.auth.authenticate`.
If you want to require two-factor authentication in the default login view, the
easiest way is to use :class:`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
:data:`~django_otp.forms.otp_verification_failed` signal is emitted. The form
can be used with :class:`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)),
)
.. autoclass:: django_otp.forms.OTPAuthenticationForm
Following is a sample template snippet that's designed for
:class:`~django_otp.forms.OTPAuthenticationForm`:
.. code-block:: html
<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 :class:`~django_otp.forms.OTPAuthenticationForm` for
your normal login views, django-otp includes an
:class:`~django.contrib.admin.AdminSite` subclass for admin integration.
.. autoclass:: django_otp.admin.OTPAdminSite
:members: name, login_form, login_template, has_permission
.. autoclass:: django_otp.admin.OTPAdminAuthenticationForm
Django has a mechanism for :ref:`overriding-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 :ref:`addstatictoken` management command to bootstrap
yourself in.
As a convenience, :class:`~django_otp.admin.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 :class:`django_otp.forms.OTPTokenForm`.
.. autoclass:: django_otp.forms.OTPTokenForm
Signals
~~~~~~~
.. data:: django_otp.forms.otp_verification_failed
This signal is sent when an OTP verification attempt fails. The source of
the signal is :class:`~django_otp.forms.OTPAuthenticationFormMixin`,
therefore it will generally be emitted only if using the provided
:class:`~django_otp.forms.OTPAuthenticationForm` or
:class:`~django_otp.forms.OTPTokenForm`. 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 :class:`~django_otp.forms.OTPAuthenticationForm`
and :class:`~django_otp.forms.OTPTokenForm` is implemented in a mixin class:
.. autoclass:: django_otp.forms.OTPAuthenticationFormMixin
.. _Low-Level API:
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
:meth:`~django.db.models.query.QuerySet.select_for_update` to prevent
concurrent access. Relevant APIs below have a ``for_verify`` parameter for
this purpose.
.. autofunction:: django_otp.devices_for_user
.. autofunction:: django_otp.user_has_device
.. autofunction:: django_otp.verify_token
.. autofunction:: django_otp.match_token
.. autofunction:: django_otp.login
.. autoclass:: django_otp.models.Device
:members: is_interactive, generate_is_allowed, generate_challenge, verify_token, verify_is_allowed, persistent_id, from_persistent_id
.. autoclass:: django_otp.models.DeviceManager
:members: devices_for_user
.. autoclass:: django_otp.models.GenerateNotAllowed
:members:
.. autoclass:: django_otp.models.VerifyNotAllowed
:members:
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:
.. decorator:: django_otp.decorators.otp_required([redirect_field_name='next', login_url=None, if_configured=False])
Similar to :func:`~django.contrib.auth.decorators.login_required`, but
requires the user to be :term:`verified`. By default, this redirects users
to :setting:`OTP_LOGIN_URL`.
:param if_configured: If ``True``, an authenticated user with no confirmed
OTP devices will be allowed. Default is ``False``.
:type if_configured: bool
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 :class:`~django_otp.models.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 <http://pypi.python.org/pypi/django-agent-trust>`_ and
`django-otp-agents <http://pypi.python.org/pypi/django-otp-agents>`_.
.. _Managing Devices:
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 :class:`~django_otp.models.Device`-derived model objects, so it will be
easy to implement. Be sure to note the :ref:`warning <unsaved_device_warning>`
about unsaved :class:`~django_otp.models.Device` objects.
|