Enforced OTP once configured

68 views
Skip to first unread message

Karl Goetz

unread,
Mar 8, 2018, 12:20:49 AM3/8/18
to django-otp
Hi,

In the last couple of months the question of how to require uses configured OTP to use their OTP has been asked several times, twice in [0] then twice again in [1] but none of the posters shared their solutions.


I needed to do this myself and tried to do it within the following requirements:


Goals:

  • if configured, applies globally
  • if not configured, does not interfere
  • does not require anything client side (ie cookie)
  • does not require multiple views (preferred)


My first got the first three points, but required a tricky dance [2]

- if request.GET.get('next) is set, store it for later

- then forcibly redirect user to my test view

- let that be captured by django-otp or let directly through

- redirect to the page we stored first time


[2] https://gitlab.com/goetzk/django-otp-vip/commit/2bfb89e0bf405864077c7683cda51f1102f12021


I've just come up with what I think is a better solution so I'm going to share it with the group - I'm happy to receive some pointers on how to improve it too.


Its a single ~50 line piece of middleware [3] which can be installed after django_otp.middleware.OTPMiddleware and ensures any user that has an otp device and is authenticated is sent to the otp login form.


[3] https://gitlab.com/goetzk/django-otp-vip/commit/787c89c37821b48151bc77c7e37ba2ebba09f401



Peter: Is this something that (with changes) you'd be happy to merge in to django-otp?


thanks,

kk




Because links break over time, this is the code as committed just now:


# Set up logging first thing

import logging

logger = logging.getLogger(__name__)


from django.core.urlresolvers import reverse

from django.http import HttpResponseRedirect


from django_otp import user_has_device, _user_is_anonymous, _user_is_authenticated


# https://stackoverflow.com/questions/2916966/non-global-middleware-in-django

# was the best help I found here, with several good posts.


otp_login_url = reverse('run_multi_factor') # This should probably be settings.OTP_LOGIN

excluded_urls_list = ['/accounts/logout/', otp_login_url]


class MandatoryOtpMiddleware(object):

  """Ensure users configured for OTP are using it.


  This middleware uses data set by django_otp.middleware.OTPMiddleware so must

  be installed after it in Django middleware settings.


  """


  def process_request(self, request):

    """

    Automatically ensure users who should be logged in via OTP are.

    """

    current_url = request.path_info

    # We don't need to worry about annon users

    if _user_is_anonymous(request.user):

      return None


    # A select few views can't be captured. For example

    # * Logout means we can never leave the site

    # * OTP login means a redirect loop when viewing the otp login url.

    if current_url in excluded_urls_list:

      logger.debug('Currently requested url ({0}) is in excluded urls list ({1})'.format(current_url, excluded_urls_list))

      return None


    # Only need to check this if they are logged in

    if _user_is_authenticated(request.user):

      # If they have at least one device they should be logged in via OTP

      if user_has_device(request.user):

        if not request.user.is_verified():

          logger.debug('{0} is authenticated but not verified. Redirecting to {1}'.format(request.user, otp_login_url))

          return HttpResponseRedirect(otp_login_url)

        # else, they are verified and are OK

      # else they have no device; they don't need to have OTP




Peter Sagerson

unread,
Mar 13, 2018, 12:23:05 PM3/13/18
to djang...@googlegroups.com
My first instinct on this sort of thing is that it may be more appropriate to django-two-factor-auth or a similarly high-level framework. django-otp itself generally tries to stick to basic tools without mixing in too much idiosyncratic policy.

If I wanted to add an enforcement policy like this, another approach I might take is to build a simple auth backend that implements the authorization APIs and raises PermissionDenied for configured, unverified users.[1] Add this to the top of AUTHENTICATION_BACKENDS and applicable users (other than superusers) will never pass any permission check. This might help to maintain a cleaner separation of authentication and authorization policies and wouldn't require any URL exceptions or similar shenanigans.

Of course, any approach along either of these lines is just a fallback. I'm also interested in some kind of mandatory access control in Django, but I've never come up with a really satisfying answer. One approach I've considered is an auditing tool that would enumerate all views in a project and identify those that are public or otherwise lack an explicit ACL policy. Of course, this is very project-specific--perhaps based on a class-based view hierarchy--and I've only speculated about the implementation.

Thanks,
Peter




--
You received this message because you are subscribed to the Google Groups "django-otp" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-otp+...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Karl Goetz

unread,
Mar 14, 2018, 6:25:05 PM3/14/18
to djang...@googlegroups.com, Peter Sagerson
On 14/3/18 03:22, Peter Sagerson wrote:
> My first instinct on this sort of thing is that it may be more
> appropriate to django-two-factor-auth or a similarly high-level
> framework. django-otp itself generally tries to stick to basic tools
> without mixing in too much idiosyncratic policy.

No worries, thought that might be the case from prior observations but
still thought it worth offering.
I am planning to look at two-factor in future but it was out of scope
for my current work.

> If I wanted to add an enforcement policy like this, another approach I
> might take is to build a simple auth backend that implements the
> authorization APIs and raises PermissionDenied for configured,
> unverified users.[1] Add this to the top of AUTHENTICATION_BACKENDS and
> applicable users (other than superusers) will never pass any permission
> check. This might help to maintain a cleaner separation of
> authentication and authorization policies and wouldn't require any URL
> exceptions or similar shenanigans.

I didn't look at this, though is a good (probably better) approach. If I
have need to replace what I'm doing in future I can look at this instead.

thanks,
kk
>> <mailto:goetz...@gmail.com>> wrote:

>> I've just come up with what I think is a better solution so I'm going
>> to share it with the group - I'm happy to receive some pointers on how
>> to improve it too.
>>
>> Its a single ~50 line piece of middleware [3] which can be installed
>> after django_otp.middleware.OTPMiddleware and ensures any user that
>> has an otp device and is authenticated is sent to the otp login form.

PS.
If anyone looks at this in future, I've re-factored the middleware
slightly so its easier to subclass and override the checks being applied
to users (enabling site specific checks).

kk

signature.asc
Reply all
Reply to author
Forward
0 new messages