Browse Source

Strong Authentication using "django-otp". Support of yubikeys using
"django-otp" plugin "django-otp-yubikey". If setting "ENABLE_STRONG_AUTH"
is True, strong authentication is used on admin backend. Only users with
a token will be allowed to access it.

Signed-off-by: CapsLock <faimaison@legeox.net>

CapsLock 10 years ago
parent
commit
b156ef8638

+ 53 - 46
coin/members/templates/members/registration/login.html

@@ -2,54 +2,61 @@
 {% load i18n %}
 {% load i18n %}
 
 
 {% block content %}
 {% block content %}
+    <div class="row">
+        <div class="medium-7 columns">
+            <h2>Connexion</h2>
+            <form method="post" action="{% block login_form_url %}{% url 'members:django.contrib.auth.views.login' %}{% endblock %}">
+                {% csrf_token %}
+                {% if form.errors %}
+                <div class="alert-box alert">
+                    {% if form.errors.items|length == 1 %}
+                        {% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}
+                    {% endif %}<br/>
+                    {% for error in form.non_field_errors %}{{ error|escape }}{% endfor %}
+                </div>
+                {% endif %}
 
 
-<div class="row">
-    <div class="medium-7 columns">
-        <h2>Connexion</h2>
+                <table width="100%">
+                    {% if form.username.label_tag %}
+                        <tr>
+                            <td>{{ form.username.label_tag }}</td>
+                            <td>{{ form.username }}
+                                {% if form.username.errors %}
+                                    <span class="error">{% for error in form.username.errors %}{{ error|escape }}{% endfor %}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    {% endif %}
+                    {% if form.password.label_tag %}
+                        <tr>
+                            <td>{{ form.password.label_tag }}
+                            <small><a href="{% url 'members:password_reset' %}" tabindex="100">Mot de passe oublié ?</a></small></td>
+                            <td>{{ form.password }}
+                                {% if form.password.errors %}
+                                    <span class="error">{% for error in form.password.errors %}{{ error|escape }}{% endfor %}</span>
+                                {% endif %}
+                            </td>
+                        </tr>
+                    {% endif %}
+                    {% block additional_fields %}
+                    {% endblock %}
+                </table>
+                {% block additional_inputs %}
+                    <input type="submit" value="Coinnexion" class="button radius"/>
+                {% endblock %}
+                <input type="hidden" name="next" value="{{ next }}" />
+            </form>
+        </div>
 
 
-        <form method="post" action="{% url 'members:django.contrib.auth.views.login' %}">
-            {% csrf_token %}
-            {% if form.errors %}
-        	<div class="alert-box alert">
-                {% if form.errors.items|length == 1 %}
-                    {% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}
-                {% endif %}<br/>
-                {% for error in form.non_field_errors %}{{ error|escape }}{% endfor %}
+        {% if form.username.label_tag %}
+            <div class="medium-5 columns">
+                <div class="panel callout" id="newcomers">
+                    <h3>Nouvel adhérent ?</h3>
+                    <p>Pour votre première connexion, il faut définir votre mot de passe.<br />
+                    <a href="{% url 'members:password_reset' %}"><i class="fa fa-arrow-right"></i>
+                    Initialiser mon mot de passe</a></p>
+                </div>
             </div>
             </div>
-        	{% endif %}
-
-        	<table width="100%">
-            	<tr>
-            		<td>{{ form.username.label_tag }}</td>
-            		<td>{{ form.username }}
-                        {% if form.username.errors %}
-                            <span class="error">{% for error in form.username.errors %}{{ error|escape }}{% endfor %}</span>
-                        {% endif %}
-                    </td>
-            	</tr>
-            	<tr>
-            		<td>{{ form.password.label_tag }}
-                    <small><a href="{% url 'members:password_reset' %}" tabindex="100">Mot de passe oublié ?</a></small></td>
-            		<td>{{ form.password }}
-                        {% if form.password.errors %}
-                            <span class="error">{% for error in form.password.errors %}{{ error|escape }}{% endfor %}</span>
-                        {% endif %}
-                    </td>
-            	</tr>
-        	</table>
-        	<input type="submit" value="Coinnexion" class="button radius"/>
-        	<input type="hidden" name="next" value="{{ next }}" />
-    	</form>
-    </div>
-    <div class="medium-5 columns">
-        <div class="panel callout" id="newcomers">
-            <h3>Nouvel adhérent ?</h3>
-            <p>Pour votre première connexion, il faut définir votre mot de passe.<br />
-            <a href="{% url 'members:password_reset' %}"><i class="fa fa-arrow-right"></i>
- Initialiser mon mot de passe</a></p>
-        </div>
+        {% endif %}
     </div>
     </div>
-
-</div>
-
 {% endblock %}
 {% endblock %}

+ 34 - 0
coin/members/templates/members/registration/login_otp.html

@@ -0,0 +1,34 @@
+{% extends "members/registration/login.html" %}
+{% load i18n %}
+
+
+{% block login_form_url %}
+    {% url 'members:django_otp.views.login' %}
+{% endblock %}
+
+{% block additional_fields %}
+    {% if form.otp_device.field.choices %}
+        <tr>
+            <td>{{ form.otp_device.label_tag }}</td>
+            <td>{{ form.otp_device }}</td>
+        </tr>
+        <tr>
+            <td>{{ form.otp_token.label_tag }}
+            <td>{{ form.otp_token }}
+                {% if form.otp_token.errors %}
+                    <span class="error">{% for error in form.otp_token.errors %}{{ error|escape }}{% endfor %}</span>
+                {% endif %}
+            </td>
+        </tr>
+    {% else %}
+        <tr>
+            <td colspan="2">{% trans "This section needs you to authenticate with a token. As you have no token registered to your account, you cannot access this section. Please contact us." %}</td>
+        </tr>
+    {% endif %}
+{% endblock %}
+
+{% block additional_inputs %}
+    {% if form.otp_device.field.choices %}
+        <input type="submit" value="Coinnexion" class="button radius"/>
+    {% endif %}
+{% endblock %}

+ 3 - 0
coin/members/urls.py

@@ -13,6 +13,9 @@ urlpatterns = patterns(
     url(r'^login/$', 'django.contrib.auth.views.login',
     url(r'^login/$', 'django.contrib.auth.views.login',
         {'template_name': 'members/registration/login.html'},
         {'template_name': 'members/registration/login.html'},
         name='login'),
         name='login'),
+    url(r'^login_otp/$', 'django_otp.views.login',
+        {'template_name': 'members/registration/login_otp.html'},
+        name='login_otp'),
     url(r'^logout/$', 'django.contrib.auth.views.logout_then_login',
     url(r'^logout/$', 'django.contrib.auth.views.logout_then_login',
         name='logout'),
         name='logout'),
 
 

+ 0 - 3
coin/members/views.py

@@ -1,13 +1,10 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-
 from django.template import RequestContext
 from django.template import RequestContext
 from django.shortcuts import render_to_response
 from django.shortcuts import render_to_response
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.decorators import login_required
-from django.http import Http404
 from django.conf import settings
 from django.conf import settings
 
 
-
 @login_required
 @login_required
 def index(request):
 def index(request):
     has_isp_feed = 'isp' in [k for k, _, _ in settings.FEEDS]
     has_isp_feed = 'isp' in [k for k, _, _ in settings.FEEDS]

+ 31 - 6
coin/settings.py

@@ -34,6 +34,12 @@ DATABASES = {
 # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
 # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
 ALLOWED_HOSTS = []
 ALLOWED_HOSTS = []
 
 
+# Enable strong authentication (username + password + OTP)
+# when enabled, strong authentication is done via a Yubikey hardware token
+# OTP is validated by a local or a remote service (configuration is done
+# through django admin pages)
+ENABLE_STRONG_AUTH = False
+
 # Local time zone for this installation. Choices can be found here:
 # Local time zone for this installation. Choices can be found here:
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # although not all choices may be available on all operating systems.
 # although not all choices may be available on all operating systems.
@@ -58,7 +64,9 @@ USE_L10N = True
 USE_TZ = True
 USE_TZ = True
 
 
 # Default URL for login and logout
 # Default URL for login and logout
+#LOGIN_URL = '/members/login_otp'
 LOGIN_URL = '/members/login'
 LOGIN_URL = '/members/login'
+OTP_LOGIN_URL = '/members/login_otp'
 LOGIN_REDIRECT_URL = '/members'
 LOGIN_REDIRECT_URL = '/members'
 LOGOUT_URL = '/members/logout'
 LOGOUT_URL = '/members/logout'
 
 
@@ -122,6 +130,7 @@ MIDDLEWARE_CLASSES = (
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django_otp.middleware.OTPMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     # Uncomment the next line for simple clickjacking protection:
     # Uncomment the next line for simple clickjacking protection:
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -142,17 +151,28 @@ INSTALLED_APPS = (
     'django.contrib.contenttypes',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.sessions',
     'django.contrib.sites',
     'django.contrib.sites',
-    'ldapdb',  # LDAP as database backend
+
+    # LDAP as database backend
+    'ldapdb',
+
     'django.contrib.messages',
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.contrib.staticfiles',
-    # Uncomment the next line to enable the admin:
-    'django.contrib.admin',
+
+    # same as django.contrib.admin but without autodiscover() call :
+    # https://docs.djangoproject.com/en/dev/ref/contrib/admin/#discovery-of-admin-files
+    'django.contrib.admin.apps.SimpleAdminConfig',
+
     # Uncomment the next line to enable admin documentation:
     # Uncomment the next line to enable admin documentation:
     #'django.contrib.admindocs',
     #'django.contrib.admindocs',
+
     'polymorphic',
     'polymorphic',
     # 'south',
     # 'south',
-    'autocomplete_light', #Automagic autocomplete foreingkey form component
-    'activelink', #Detect if a link match actual page
+
+    #Automagic autocomplete foreingkey form component
+    'autocomplete_light',
+
+    #Detect if a link match actual page
+    'activelink',
     'coin',
     'coin',
     'coin.members',
     'coin.members',
     'coin.offers',
     'coin.offers',
@@ -161,7 +181,9 @@ INSTALLED_APPS = (
     'coin.reverse_dns',
     'coin.reverse_dns',
     'coin.configuration',
     'coin.configuration',
     'coin.vpn',
     'coin.vpn',
-    'coin.isp_database'
+    'coin.isp_database',
+    'django_otp',
+    'otp_yubikey'
 )
 )
 
 
 # A sample logging configuration. The only tangible logging
 # A sample logging configuration. The only tangible logging
@@ -249,3 +271,6 @@ try:
     from settings_local import *
     from settings_local import *
 except ImportError:
 except ImportError:
     pass
     pass
+
+# if ENABLE_STRONG_AUTH:
+#     LOGIN_URL = OTP_LOGIN_URL

+ 14 - 12
coin/urls.py

@@ -1,21 +1,24 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-
 from django.conf import settings
 from django.conf import settings
 from django.conf.urls import patterns, include, url
 from django.conf.urls import patterns, include, url
 from django.conf.urls.static import static
 from django.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-
 from coin import views
 from coin import views
-
-
 import autocomplete_light
 import autocomplete_light
+from django_otp.admin import OTPAdminSite
+from django.contrib import admin
+from coin.isp_database.views import isp_json
+
 autocomplete_light.autodiscover()
 autocomplete_light.autodiscover()
 
 
-from django.contrib import admin
-admin.autodiscover()
+admin_site_name = 'admin'
 
 
-from coin.isp_database.views import isp_json
+if settings.ENABLE_STRONG_AUTH:
+    admin.site = OTPAdminSite(OTPAdminSite.name)
+    admin_site_name = OTPAdminSite.name
+
+admin.autodiscover()
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
     '',
     '',
@@ -24,10 +27,10 @@ urlpatterns = patterns(
     url(r'^isp.json$', isp_json),
     url(r'^isp.json$', isp_json),
     url(r'^members/', include('coin.members.urls', namespace='members')),
     url(r'^members/', include('coin.members.urls', namespace='members')),
     url(r'^billing/', include('coin.billing.urls', namespace='billing')),
     url(r'^billing/', include('coin.billing.urls', namespace='billing')),
-    url(r'^subscription/', include('coin.offers.urls', namespace='subscription')),
+    url(r'^subscription/', include('coin.offers.urls',
+                                   namespace='subscription')),
     url(r'^vpn/', include('coin.vpn.urls', namespace='vpn')),
     url(r'^vpn/', include('coin.vpn.urls', namespace='vpn')),
-
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', include(admin.site.urls, app_name=admin_site_name)),
 
 
     # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
     # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
 
 
@@ -38,5 +41,4 @@ urlpatterns = patterns(
 )
 )
 
 
 urlpatterns += staticfiles_urlpatterns()
 urlpatterns += staticfiles_urlpatterns()
-urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 4 - 0
requirements.txt

@@ -14,3 +14,7 @@ django-localflavor==1.1
 -e git+https://github.com/chrisglass/xhtml2pdf@a5d37ffd0ccb0603bdf668198de0f21766816104#egg=xhtml2pdf-master
 -e git+https://github.com/chrisglass/xhtml2pdf@a5d37ffd0ccb0603bdf668198de0f21766816104#egg=xhtml2pdf-master
 -e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
 -e git+https://github.com/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
 feedparser
 feedparser
+YubiOTP==0.2.1
+pycrypto==2.6.1
+django-otp==0.2.7
+django-otp-yubikey==0.2.0