pythondjangotwo-factor-authenticationyubico

Django and Yubikey integrate


I want to integrate Yubikey with my Django application. Whenever I try to add a 2FA Yubikey: localhost:8080/account/two_factor/setup/ I get a 403 HTTP Forbidden error. How do I implement the Yubikey in Django, because when I use these tutorials then the implementation fails.

When I follow the example here to do the same I get a Server Error 500

I followed these tutoriols:

This is what I have done:

pip install django-two-factor-auth
pip install django-otp-yubikey
pip install django-otp

I use Python 3.7.3 and Django 3.0.8

In my settings.py I added:

INSTALLED_APPS = [
    ...
    # OTP
    'django_otp',
    'django_otp.plugins.otp_static',
    'django_otp.plugins.otp_totp',
    'two_factor',
    'otp_yubikey',
    ...
]

MIDDLEWARE = [
    ...
    'django_otp.middleware.OTPMiddleware',
    ...
]

LOGIN_URL = 'two_factor:login'

TWO_FACTOR_PATCH_ADMIN = True

In my urls.py I added:

from two_factor.urls import urlpatterns as tf_urls
from django_otp.views import LoginView
...

urlpatterns = [
    ...
    # OTP
    path('', include(tf_urls)),
    ...
]

And in my views.py I have added:

from django_otp.decorators import otp_required

@otp_required
def home_view(request):
    return render(request, 'home.html', {'result': 'test')

My home.html is simple:

<html>
    <head></head>
    <body><p>test</p></body>
</html>

Stacktrace:

ERROR 2020-07-20 14:14:24,571 log 35773 123145342668800 Internal Server Error: /account/two_factor/setup/
Traceback (most recent call last):
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/views/generic/base.py", line 71, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/utils/decorators.py", line 43, in _wrapper
return bound_method(*args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/utils/decorators.py", line 43, in _wrapper
return bound_method(*args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
return view_func(request, *args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/formtools/wizard/views.py", line 244, in dispatch
response = super().dispatch(request, *args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/views/generic/base.py", line 97, in dispatch
return handler(request, *args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/two_factor/views/utils.py", line 156, in post
return super().post(*args, **kwargs)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/formtools/wizard/views.py", line 294, in post
if form.is_valid():
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/forms/forms.py", line 180, in is_valid
return self.is_bound and not self.errors
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/forms/forms.py", line 175, in errors
self.full_clean()
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/forms/forms.py", line 376, in full_clean
self._clean_fields()
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/django/forms/forms.py", line 397, in _clean_fields
value = getattr(self, 'clean_%s' % name)()
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/two_factor/forms.py", line 84, in clean_token
return super().clean_token()
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/two_factor/forms.py", line 70, in clean_token
if not self.device.verify_token(token):
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/otp_yubikey/models.py", line 280, in verify_token
response = client.verify(token)
File "/Users/user_anon/work_folder/home_dir/venv/lib/python3.7/site-packages/yubiotp/client.py", line 48, in verify
stream = urlopen(url)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 222, in urlopen
return opener.open(url, data, timeout)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 531, in open
response = meth(req, response)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 641, in http_response
'http', request, response, code, msg, hdrs)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 569, in error
return self._call_chain(*args)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 503, in _call_chain
result = func(*args)
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/urllib/request.py", line 649, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden

Solution

  • Sigh, Ok so I edited the client.py in yubiotp so have full control over the URL that is used in urlopen

    replaced

    stream = urlopen(url)
    

    with

    from app import views as v_yow
    ...
    stream = v_yow.test(url)
    

    The views.py:

    from six.moves import xrange
    from six.moves.urllib.parse import urlencode
    from six.moves.urllib.request import urlopen
    
    import urllib
    
    
    def test(url):
        """
        """
        print('Testing 123')
        print(url)
        url_without_http = url[4:]
        url_with_https = 'https' + url_without_http
    
        stream = urlopen(url_with_https)
        return stream
    

    And this works excellent. So the problem here is that the requested URL was a HTTP url and not a HTTPS url. The HTTP url redirects to HTTPS but fails to word in the lib urlopen(url).

    So to make this work without having this weird "fix" make sure you set the use_ssl to True in :

    classotp_yubikey.models.ValidationService(*args, **kwargs)[source]