Parcourir la 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 il y a 10 ans
Parent
commit
b156ef8638

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

@@ -2,54 +2,61 @@
 {% load i18n %}
 
 {% 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>
-        	{% 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>
-
 {% 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',
         {'template_name': 'members/registration/login.html'},
         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',
         name='logout'),
 

+ 0 - 3
coin/members/views.py

@@ -1,13 +1,10 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-
 from django.template import RequestContext
 from django.shortcuts import render_to_response
 from django.contrib.auth.decorators import login_required
-from django.http import Http404
 from django.conf import settings
 
-
 @login_required
 def index(request):
     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
 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:
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # although not all choices may be available on all operating systems.
@@ -58,7 +64,9 @@ USE_L10N = True
 USE_TZ = True
 
 # Default URL for login and logout
+#LOGIN_URL = '/members/login_otp'
 LOGIN_URL = '/members/login'
+OTP_LOGIN_URL = '/members/login_otp'
 LOGIN_REDIRECT_URL = '/members'
 LOGOUT_URL = '/members/logout'
 
@@ -122,6 +130,7 @@ MIDDLEWARE_CLASSES = (
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django_otp.middleware.OTPMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     # Uncomment the next line for simple clickjacking protection:
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -142,17 +151,28 @@ INSTALLED_APPS = (
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.sites',
-    'ldapdb',  # LDAP as database backend
+
+    # LDAP as database backend
+    'ldapdb',
+
     'django.contrib.messages',
     '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:
     #'django.contrib.admindocs',
+
     'polymorphic',
     # '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.members',
     'coin.offers',
@@ -161,7 +181,9 @@ INSTALLED_APPS = (
     'coin.reverse_dns',
     'coin.configuration',
     'coin.vpn',
-    'coin.isp_database'
+    'coin.isp_database',
+    'django_otp',
+    'otp_yubikey'
 )
 
 # A sample logging configuration. The only tangible logging
@@ -249,3 +271,6 @@ try:
     from settings_local import *
 except ImportError:
     pass
+
+# if ENABLE_STRONG_AUTH:
+#     LOGIN_URL = OTP_LOGIN_URL

+ 14 - 12
coin/urls.py

@@ -1,21 +1,24 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-
 from django.conf import settings
 from django.conf.urls import patterns, include, url
 from django.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-
 from coin import views
-
-
 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()
 
-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(
     '',
@@ -24,10 +27,10 @@ urlpatterns = patterns(
     url(r'^isp.json$', isp_json),
     url(r'^members/', include('coin.members.urls', namespace='members')),
     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'^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')),
 
@@ -38,5 +41,4 @@ urlpatterns = patterns(
 )
 
 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/jlaine/django-ldapdb@1c4f9f29e52176f4367a1dffec2ecd2e123e2e7a#egg=django-ldapdb
 feedparser
+YubiOTP==0.2.1
+pycrypto==2.6.1
+django-otp==0.2.7
+django-otp-yubikey==0.2.0