Commits

Nuno Maltez committed 79bbf0c

Initial commit.

  • Participants
  • Tags 0.1dev

Comments (0)

Files changed (57)

+syntax: glob
+test.db
+.DS_Store
+*.pyc
+*~
+* Cognitiva (cognitiva.com) https://bitbucket.org/cogni
+* Nuno Maltez <nuno@cognitiva.com> https://bitbucket.org/nuno
+* Pedro Lima <pedro@cognitiva.com> https://bitbucket.org/pvl
+0.1
+---
+
+- First release as a pypi package.
+Copyright (c) 2011, Cognitiva, Nuno Maltez, Pedro Lima and contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the authors nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include AUTHORS
+include LICENSE
+include README.rst
+include HISTORY
+recursive-include docs *
+recursive-include twostepauth/templates *
+recursive-include twostepauth/fixtures *
+recursive-include twostepauth/tests/templates *
+==================
+django-twostepauth
+==================
+
+Django application that allows user authentication with two steps for 
+additional security. The a first step with username and password and a 
+second step with a one-time code such as the codes generated by soft 
+token devices like [Google Authenticator][goog_auth].
+
+Features:
+
+    * Authentication with TOTP (Time-Based One-Time Password)
+    * Authentication HOTP (HMAC-Based One-Time Password)
+    * Support for the login in the admin site
+    * Selective activation of two-step for the admin site, the main site or both
+    * Support for authentication backup codes
+    * Automatic adjustment for clock synchronization issues
+
+Details on the installation and setup can be found in ``docs/install.rst``. An 
+example application is provided for two-step authentication integrated with 
+``django-registration`` and ``django-profiles``.
+
+[goog_auth] http://support.google.com/accounts/bin/answer.py?hl=en&answer=1066447

File docs/index.rst

+django-twostepauth documentation
+================================
+
+This documentation covers django-twostepauth, a Django application that allows
+user authentication with a first step with username and password and a 
+second step with a one time code such as the one generated by soft token
+devices like Google Authenticator.
+
+This application can be integrated with custom user registration and user
+profiles strategies and included is a demo implementation using 
+``django-registration`` and ``django-profiles`` packages.
+
+
+

File docs/install.rst

+============
+Installation
+============
+
+Package installation
+--------------------
+
+Steps for installation for ``django-twostepauth``.
+
+Pre-requisites: 
+	
+	Python 2.5 or greater
+    Django 1.3 or greater
+
+To install it, run the following command inside this directory::
+
+    python setup.py install
+
+If you have the Python ``easy_install`` utility available, you can
+also type the following to download and install in one step::
+
+   easy_install -Z django-twostepauth
+
+(the ``-Z`` flag is required to force ``easy_install`` to do a normal
+source install rather than a zipped egg)
+
+Or if you're using ``pip``::
+
+    pip install django-twostepauth
+
+Or if you'd prefer you can simply place the included ``twostepauth``
+directory somewhere on your Python path, or symlink to it from
+somewhere on your Python path.
+
+Package configuration
+---------------------
+
+Add the ``twostepauth`` package to the list of ``INSTALLED_APPS``.
+
+The ``AUTHENTICATION_BACKENDS`` must set as the authentication backend the 
+supplied backend ``twostepauth.auth_backend.TwoStepAuthBackend``. It 
+important that the standard backend is not kept in the sequence of 
+backends as this would authenticate the users before the second step.
+
+To allow the two-step authentication for users set ``TWOSTEPAUTH_FOR_USERS`` 
+to True. To activate two-step authentication for the admin section of
+the site set ``TWOSTEPAUTH_FOR_ADMIN`` to True.
+
+Two-step authentication needs to store extra information for each user. This is kept 
+in the user profile, so you'll need to create a profile model that inherits from
+``twostepauth.models.TwoStepAuthBaseProfile``:
+
+    from twostepauth.models import TwoStepAuthBaseProfile
+
+    class UserProfile(TwoStepAuthBaseProfile):
+        user = models.OneToOneField('auth.User')
+
+and in ``settings.py`` indicate that this model is the user profile model:
+
+    AUTH_PROFILE_MODULE = 'myapp.UserProfile'
+
+
+In the urls configuration set the two-step login views 
+
+    url(r'^accounts/login/$', 
+        'twostepauth.views.login_step_one', 
+        name='auth_login'),
+    url(r'^accounts/login/step_two$', 
+        'twostepauth.views.login_step_two', 
+        name='login_step_two')
+
+The ``twostepauth`` also includes a view for the user profile management of
+the two-step authentication. Example setup for this view:
+
+    url(r'^profiles/twostepauth/$', 
+        'twostepauth.views.twostepauth_profile', 
+        name='twostepauth_profile'),
+
+You will need to create a template named ``twostepauth/profile.html``. You can
+find an example in the ``exampleapp`` application included in this distribution.
+
+
+Demo Application
+----------------
+
+The demo application is an setup example of authentication, registration and profile 
+using the ``django-registration`` and ``django-profiles`` packages.
+
+To try the demo application install the above packages (note that django-registration
+must be version 0.8 alpha or above).
+
+	pip install https://bitbucket.org/ubernostrum/django-registration/downloads/django-registration-0.8-alpha-1.tar.gz
+
+    pip install django-profiles
+
+
+
+
+Management Command
+------------------
+
+The Django management command ``generate_twostepauth_secret`` can be used to activate two-step
+authentication for a user and generate the secret key from the command line:
+
+    python manage.py generate_twostepauth_secret <username>

File exampleapp/__init__.py

Empty file added.

File exampleapp/demoapp/__init__.py

+
+import signals

File exampleapp/demoapp/models.py

+from django.db import models
+from twostepauth.models import TwoStepAuthBaseProfile
+
+
+
+class UserProfile(TwoStepAuthBaseProfile):
+    user = models.OneToOneField('auth.User')
+    website = models.CharField(max_length=255, blank=True)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('profiles_profile_detail', (), { 'username': self.user.username })
+

File exampleapp/demoapp/signals.py

+from django.db import models
+from django.db.models.signals import post_save
+from django.contrib.auth.models import User
+from .models import UserProfile
+
+#to automatically create a userprofile when a user is created
+def create_profile(sender, **kw):
+    user = kw["instance"]
+    if kw["created"]:
+        profile = UserProfile(user=user)
+        profile.save()
+
+post_save.connect(create_profile, sender=User, dispatch_uid="users-profilecreation-signal")

File exampleapp/demoapp/static/style.css

+body { width: 800px; margin: 30px auto 50px; font-family: 'Georgia', serif; font-size: 17px; color: #000; }
+a            { color: #004B6B; }
+a:hover      { color: #6D4100; }
+h1, h2, h3, h4   { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
+h1           { font-size: 30px; margin: 15px 0 5px 0; }
+h2           { font-size: 30px; margin: 15px 0 5px 0; }
+#header { float: right; }
+#header-nav ul {  }
+#header-nav li { float: left; list-style: none; margin-left: 7px; }
+#header-nav a { font-style: italic; margin-right:7px; }
+#body { clear: both; padding-top: 20px; }
+
+label { display: block; float: left; clear: left; width: 10em; padding-right: 1em; text-align: left; 
+       line-height: 1.8em }
+input { display: block; float: left; line-height: 1.8em }
+select { display: block; float: left; line-height: 1.8em }
+br { clear: both; }
+fieldset { border: 0px; margin: 0; padding: 0; }
+legend { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 30px; margin: 15px 0 5px 0; } 
+li { list-style: none; }
+ul.errorlist { color:red; }
+.submit { padding-top: 10px; }

File exampleapp/demoapp/templates/base.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+"http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+  <title>{% block title %}Two-Step Authentication Demo{% endblock %}</title>
+  <script type="text/javascript"
+   src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+  <link rel="stylesheet" type="text/css" href="/static/style.css" />
+  {% block meta %}{% endblock %}
+</head>
+<body>
+  <div id="header">
+  	<ul id="header-nav">
+	    <li><a href="/">home</a>//</li>	
+	    <li><a href="{% url profiles_profile_list %}">profiles list</a>//</li>
+	  {% if user.is_authenticated %}
+	    <li><a href="{% url profiles_profile_detail user.username %}">view profile</a>//</li>
+	    <li><a href="{% url profiles_edit_profile %}">edit profile</a>//</li>
+		{% if user.get_profile.twostep_auth_enabled %}
+	    <li><a href="{% url twostepauth_profile %}">two-step authentication</a>//</li>
+		{% endif %}
+		<li><a href="/accounts/logout/">logout</a></li>
+	  {% else %}
+		<li><a href="/accounts/login/">login</a>//</li>
+		<li><a href="/accounts/register/">register</a></li>
+	  {% endif %} 
+	</ul>
+  </div>
+
+  <div id="body">
+    {% block body %}
+      <div class="content_title">
+        {% block content_title %}{% endblock %}
+      </div>
+      <div class="content">
+        {% block content %}{% endblock %}
+      </div>
+    {% endblock %}
+  </div>
+</body>
+</html>

File exampleapp/demoapp/templates/index.html

+{% extends "base.html" %}
+
+{% block title %}Two-step auth demo application with django-registration{% endblock %}
+
+{% block content_title %}<h2>Two-step auth demo application with django-registration</h2>{% endblock %}
+
+{% block content %}
+<p> Welcome to the two-step authentication demo application for integration with django-registration and django-profiles.</p>
+
+{% endblock %}

File exampleapp/demoapp/templates/profiles/create_profile.html

+{% extends "base.html" %}
+{% block content %}
+<form action="{% url profiles_create_profile %}" method="post">{% csrf_token %}
+{{ form.as_p }}
+<input type="submit" value="Submit" >
+</form>
+{% endblock %}

File exampleapp/demoapp/templates/profiles/edit_profile.html

+{% extends 'base.html' %}
+
+{% block title %}Edit profile{% endblock %}
+
+{% block content_title %}<h2>Edit  profile</h2>{% endblock %}
+
+{% block content %}
+
+<form action="" method="post" id="twostepauth_change_form">
+
+  <fieldset>
+    {{ form.as_p }}
+  </fieldset>
+
+  <div class="submit">
+  <input type="submit" value="Change" />
+  </div>
+</form>
+
+{% endblock %}

File exampleapp/demoapp/templates/profiles/profile_detail.html

+{% extends 'base.html' %}
+
+{% block title %}{{ profile.user.username }}'s profile.{% endblock %}
+
+{% block content_title %}<h2>Profile of {{ profile.user.username }} {% if profile.user.get_full_name %}({{ profile.user.get_full_name }}){% endif %}</h2>{% endblock %}
+
+{% block content %}
+<div class="white-box">
+
+  <div id="details">
+    {% if profile.user.get_full_name %}
+    <p><strong>Name</strong><br /> {{ profile.user.get_full_name }}</p>
+    {% endif %}
+    {% if profile.user.email %}
+    <p><strong>Email</strong><br />{{ profile.user.email }}</p>
+    {% endif %}
+    {% if profile.website %}
+    <p><strong>Website</strong><br /> <a href="{{ profile.website }}">{{ profile.website }}</a></p>
+    {% endif %}
+  </div>
+</div>
+{% endblock %}

File exampleapp/demoapp/templates/profiles/profile_list.html

+{% extends 'base.html' %}
+
+{% block title %}Profile List{% endblock %}
+
+{% block content_title %}<h2>Profile List</h2>{% endblock %}
+
+{% block content %}
+
+<ul>
+{% for obj in object_list %}
+<li><a href="{% url profiles_profile_detail obj.user.username %}">{{ obj.user.username }}</a></li>
+{% endfor %}
+</ul>
+
+{% endblock %}

File exampleapp/demoapp/templates/registration/activate.html

+{% extends "base.html" %}
+{% block content %}
+<h1>Hmmmm</h1>
+<p>The key you used - <code>{{activation_key}}</code> - is not valid.
+</p>
+{% endblock %}

File exampleapp/demoapp/templates/registration/activation_complete.html

+{% extends "base.html" %}
+{% block content %}
+<h1>Welcome!</h1>
+<p>We hope you'll enjoy this website</p>
+{% endblock %}

File exampleapp/demoapp/templates/registration/activation_email.txt

+Dear user:
+
+This e-mail address has been submitted for registration with {{site.name}}. 
+To complete your registration, click the following link: http://{{site.domain}}{% url registration_activate activation_key %}
+
+Sincerely,
+{{site.name}} Administration Team

File exampleapp/demoapp/templates/registration/activation_email_subject.txt

+{{site.name}} - Confirm Registration

File exampleapp/demoapp/templates/registration/login.html

+{% extends "base.html" %}
+{% load url from future %}
+
+{% block content %}
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action="{% url 'auth_login' %}">
+{% csrf_token %}
+<legend>Login</legend>
+<label for="id_username">{{ form.username.label_tag }}</label>
+{{ form.username }}
+<br>
+<label for="id_password">{{ form.password.label_tag }}</label>
+{{ form.password }}
+<br>
+<div class="submit">
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{{ next }}" />
+<br><br>
+New user? <a href="{% url 'registration_register' %}">Register</a>
+</div>		
+</form>
+{% endblock %}

File exampleapp/demoapp/templates/registration/logout.html

+{% extends "base.html" %}
+{% block content %}
+<h1>You have been logged out</h1>
+<p>We hope you'll return soon. We already miss you :(</p>
+<p><a href="{% url home %}">Home</a></p>
+{% endblock %}

File exampleapp/demoapp/templates/registration/registration_complete.html

+{% extends "base.html" %}
+{% block content %}
+<h1>Registration almost complete</h1>
+<p>You must now activate your account before you can login</p>
+<p>An email has been sent to your address with further instructions.</p>
+{% endblock %}

File exampleapp/demoapp/templates/registration/registration_form.html

+{% extends "base.html" %}
+{% block content %}
+<form action="" method="post">{% csrf_token %}
+<legend>Register</legend>
+{{ form.as_p }}
+<br>
+<p class="submit">
+<input type="submit" value="Submit" />
+</p>
+</form>
+{% endblock %}

File exampleapp/demoapp/templates/twostepauth/profile.html

+{% extends 'base.html' %}
+
+{% block title %}Manage two-step authentication{% endblock %}
+
+{% block content_title %}<h2>Account {{ profile.user.username }} - Manage Two-Step Authentication</h2>{% endblock %}
+
+{% block content %}
+
+<script>
+$(document).ready( function() {
+	$('a#backcode_toggle').click( function() {
+		$('#backupcodes').toggle();
+		return false;
+	});	
+});
+</script>
+
+<form action="" method="post" id="twostepauth_change_form">
+  <fieldset>
+	{% if not chart_url %}
+     <p>With 2-Step authentication, the server will check first the username/email and password 
+		and then will ask for an authentication token generated by the Authenticator App installed in 
+		a mobile device (during 30 days logins from the same computer will not need the token). Using
+		2-Step authentication will make the account more secure.</p>
+	{% endif %}
+    {% csrf_token %}
+    {{ form.as_p }}
+  </fieldset>
+  {% if chart_url %}
+  <p>
+	<label>QR code to setup the authenticator app</label>
+	<img src="{{ chart_url }}">
+  </p>
+  <p>
+    <label>Secret Key</label>
+    {{ profile.tsa_secret }}
+  </p>
+  <p>
+	<label>Backup codes <br> &nbsp;</label>
+	<a id="backcode_toggle" href="">Display/hide backup codes</a>
+	<span id="backupcodes" style="display:none">
+		<br>
+		{% for code in profile.get_backup_codes %}
+		  {{ code }} &nbsp;
+		  {% if forloop.counter == 5 %}<br>{% endif %}
+		{% endfor %}
+	</span>
+  </p>
+  {% endif %}
+  <p>
+  <input type="submit" value="Change" />
+  </p>
+</form>
+
+{% endblock %}

File exampleapp/manage.py

+#!/usr/bin/env python
+from django.core.management import execute_manager
+import imp
+try:
+    imp.find_module('settings') # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
+    sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+    execute_manager(settings)

File exampleapp/settings.py

+# Django settings for testauth_registration project.
+import os
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
+
+ADMINS = (
+    # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': 'test.db',                      # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+# 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.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = ''
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/'
+
+# URL prefix for admin static files -- CSS, JavaScript and images.
+# Make sure to use a trailing slash.
+# Examples: "http://foo.com/static/admin/", "/static/admin/".
+ADMIN_MEDIA_PREFIX = '/static/admin/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'v!ct!bstuio_3!b_-0m744z8brv)+07xoolu@w1)7^4ql0&d*e'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+#     'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'exampleapp.urls'
+
+TEMPLATE_DIRS = (
+    os.path.join(PROJECT_ROOT, 'demoapp/templates')
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django.contrib.admin',
+    'registration',
+    'profiles',
+    'twostepauth',
+    'demoapp'
+)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
+
+AUTHENTICATION_BACKENDS = (
+    'twostepauth.auth_backend.TwoStepAuthBackend',
+)
+
+AUTH_PROFILE_MODULE = 'demoapp.UserProfile'
+LOGIN_REDIRECT_URL = '/profiles/edit/'
+
+if DEBUG:
+    # Use the Python SMTP debugging server. You can run it with:
+    # ``python -m smtpd -n -c DebuggingServer localhost:1025``.
+    EMAIL_PORT = 1025
+
+
+#django-registration specific settings
+ACCOUNT_ACTIVATION_DAYS = 1
+
+TWOSTEPAUTH_FOR_USERS = True
+TWOSTEPAUTH_FOR_ADMIN = True

File exampleapp/urls.py

+from django.views.generic.simple import direct_to_template
+from django.conf.urls.defaults import patterns, include, url
+from django.conf import settings
+from django.contrib import admin
+
+from twostepauth.forms import get_profile_form
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    url(r'^admin/', include(admin.site.urls)),
+    url(r'^accounts/login/$',
+           'twostepauth.views.login_step_one',
+           {'template_name':'registration/login.html'},
+           name='auth_login'),
+   url(r'^accounts/login/step_two$',
+           'twostepauth.views.login_step_two',
+           name='login_step_two'),
+    #registration
+    (r'^accounts/', include('registration.urls')),
+    #profiles
+    url(r'^profiles/twostepauth/$', 'twostepauth.views.twostepauth_profile', name='twostepauth_profile'),
+    url(r'^profiles/edit/$', 'profiles.views.edit_profile', 
+            {'form_class': get_profile_form() }, name='profiles_edit_profile'),
+    (r'^profiles/', include('profiles.urls')),
+    #demo application
+    url(r'^', direct_to_template, {'template':'index.html'}, name='home'),
+)
+
+if settings.DEBUG:
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$',
+         'django.views.static.serve',
+         {'document_root': settings.MEDIA_ROOT, 'show_indexes': True, }),
+)
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.1'
+
+def read(fname):
+    # read the contents of a text file
+    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+install_requires = [
+    'Django>= 1.3'
+]
+
+setup(name='django-twostepauth',
+      version=version,
+      description="Two-step authentication for Django",
+      long_description=read('README.rst'),
+      platforms=['OS Independent'],
+      classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Framework :: Django',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Internet :: WWW/HTTP',
+      ], 
+      keywords='django,authentication',
+      author='Nuno Maltez, Pedro Lima',
+      author_email='nuno@cognitiva.com, pedro@cognitiva.com',
+      url='https://bitbucket.org/cogni/django-twostepauth',
+      license='BSD',
+      packages=find_packages(exclude=['exampleapp']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=install_requires,
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )

File twostepauth/__init__.py

+

File twostepauth/admin.py

+#coding: utf-8
+from django.contrib import admin
+from .models import BlockedTimestamp, ResettingTimeSkew
+from .forms import TwoStepAdminAuthenticationForm, SingleStepAdminAuthenticationForm
+from . import settings as twostepauth_settings
+
+#change the admin forms to support two-step admin
+if twostepauth_settings.TWOSTEPAUTH_FOR_ADMIN:
+    admin.site.login_form = TwoStepAdminAuthenticationForm
+    admin.site.login_template = 'twostepauth/adminlogin.html'
+else:
+    admin.site.login_form = SingleStepAdminAuthenticationForm
+
+admin.site.register(BlockedTimestamp)
+admin.site.register(ResettingTimeSkew)

File twostepauth/auth_backend.py

+#coding: utf-8
+from django import forms
+from django.contrib.auth import authenticate
+from django.contrib.auth.backends import ModelBackend
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.models import User, check_password
+from django.contrib.auth.tokens import default_token_generator
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.translation import ugettext_lazy as _
+from .utils import RememberComputerTokenGenerator
+from . import settings as twostepauth_settings
+
+
+class TwoStepAuthBackend(ModelBackend):
+    """ 
+    Authenticates users in two-steps:
+         1. username and password validation
+         2. one-time password validation
+     """
+    token_generator = default_token_generator
+
+    def authenticate(self, username=None, password=None, token=None, code=None,
+                     remember_token=None, method='APP', force_single_step=False):
+        """
+        Authenticates a user with a regular password and optionally One-Time code.
+
+        There are three modes available, depending on what parameters the caller supplies:
+
+        1. username and password
+
+        Returns:
+            User - if the user identified by this username and password has 2-step inactive
+            None - if the user identified by this username and password has 2-step active or
+                   username + password is not a valida combination
+
+
+        2. username, password and code (otp or backup)
+
+        Returns:
+            User - if the user identified by this username and password has 2-step active and the code is valid
+                   or if the user has 2-step off
+            None - if the user identified by this username and password has 2-step active and
+                   username + password is not a valid combination or the code is invalid
+
+
+        3. username, token and code (otp or backup)
+
+        token is a token generated by self.token_generator and identifies a user that has
+        passed the first authentication step, i.e., validated a username+password
+
+        Returns:
+            User - if the user identified by this username and token has 2-step active and the code is valid
+            None - if there is no user identified by this _whatever_, if the code is invalid or if the user
+                   doesn't have two-step authentication enabled.
+
+
+        4. username, password and remember_token
+
+        remember_token is a token saved in the user cookie that allows skiping the second step of the
+        authentication for a given number of days
+
+        Returns:
+            User - if the user is identified by this username/password and the remember_token value is
+                   valid for this user
+            None - if the user/pass is wrong or if the remember_token is not valid
+
+        When the optional force_single_step is True, then username and password is enough to authenticate
+        a user. This parameter is needed to be able to separate the logins from admin or users and two
+        have different settings for these two scenarios.
+
+        TODO some sanity check of the possible combination of parameters?
+        """
+        if remember_token:
+            skip_second_step = self._check_skip_second_step(username, remember_token)
+        else:
+            skip_second_step = False
+
+        if username and password:
+            user = super(TwoStepAuthBackend, self).authenticate(username, password)
+            if user is None or force_single_step:
+                return user
+            try:
+                profile = user.get_profile()
+            except ObjectDoesNotExist:
+                # 2step not enabled - return user
+                return user
+            if profile.tsa_active and not skip_second_step and not profile.validate(code, method):
+                return None
+            return user
+        elif username and token:
+            try:
+                user = User.objects.get(username=username)
+            except User.DoesNotExist:
+                return None
+            if not self.token_generator.check_token(user, token):
+                return None
+            try:
+                profile = user.get_profile()
+            except ObjectDoesNotExist:
+                return None
+            if profile.tsa_active and profile.validate(code, method):
+                return user
+            return None
+        return None
+
+    def first_step(self, username=None, password=None):
+        """
+        Validates the username and password and returns a token that can be passed to authenticate with a
+        otp to complete the authentication process.
+        """
+        user = super(TwoStepAuthBackend, self).authenticate(username, password)
+        if user is None:
+            return None
+        return self.token_generator.make_token(user)
+
+    def _check_skip_second_step(self, username, remember_token):
+        try:
+            user = User.objects.get(username=username)
+        except User.DoesNotExist:
+            return False
+        token_generator = RememberComputerTokenGenerator()
+        return token_generator.check_token(user, remember_token)

File twostepauth/fixtures/test_users.json

+[
+    {
+        "fields": {
+            "date_joined": "2012-01-10 05:56:16", 
+            "email": "test_otp@example.com", 
+            "first_name": "", 
+            "groups": [], 
+            "is_active": true, 
+            "is_staff": false, 
+            "is_superuser": false, 
+            "last_login": "2012-01-10 05:56:16", 
+            "last_name": "", 
+            "password": "sha1$4e1f2$f6d38a7284c086eeefe9780a3880f382a0152640", 
+            "user_permissions": [], 
+            "username": "test_otp"
+        }, 
+        "model": "auth.user", 
+        "pk": 2
+    }, 
+    {
+        "fields": {
+            "date_joined": "2012-01-10 05:56:46", 
+            "email": "test_no_otp@example.com", 
+            "first_name": "", 
+            "groups": [], 
+            "is_active": true, 
+            "is_staff": false, 
+            "is_superuser": false, 
+            "last_login": "2012-01-10 05:57:04", 
+            "last_name": "", 
+            "password": "sha1$bf983$6d8e8661497bee53079f865eebdf71ee353973ca", 
+            "user_permissions": [], 
+            "username": "test_no_otp"
+        }, 
+        "model": "auth.user", 
+        "pk": 3
+    },
+    {
+        "fields": {
+            "date_joined": "2012-01-10 05:56:16", 
+            "email": "test_no_profile@example.com", 
+            "first_name": "", 
+            "groups": [], 
+            "is_active": true, 
+            "is_staff": false, 
+            "is_superuser": false, 
+            "last_login": "2012-01-10 05:56:16", 
+            "last_name": "", 
+            "password": "sha1$fe833$7c7e58550999f39d0a18d14113ee09114cde1fb4",
+            "user_permissions": [],
+            "username": "test_no_profile"
+        }, 
+        "model": "auth.user", 
+        "pk": 4
+    }, 
+    {
+        "fields": {
+            "tsa_active": false, 
+            "tsa_backup_codes": "", 
+            "tsa_hotp_counter": 1, 
+            "tsa_secret": "", 
+            "tsa_skew": 0, 
+            "user": 3
+        }, 
+        "model": "twostepauth.testprofile", 
+        "pk": 2
+    }, 
+    {
+        "fields": {
+            "tsa_active": true, 
+            "tsa_backup_codes": "89683765;88611506;92275470;66332141;40243962;31262430;67871482;74624078;37259719;87607025", 
+            "tsa_hotp_counter": 1, 
+            "tsa_secret": "XYTZDQ672RIYILW4", 
+            "tsa_skew": 0,
+            "user": 2
+        }, 
+        "model": "twostepauth.testprofile", 
+        "pk": 3
+    },
+    {
+        "fields": {
+            "timestamp": 99, 
+            "user": 2
+        }, 
+        "model": "twostepauth.blockedtimestamp", 
+        "pk": 1
+    }
+]

File twostepauth/forms.py

+from django import forms
+from django.conf import settings
+from django.contrib.auth import authenticate
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.models import User, SiteProfileNotAvailable
+from django.contrib.auth.tokens import default_token_generator
+from django.contrib.admin.forms import AdminAuthenticationForm
+from django.db.models import get_model
+from django.utils.translation import ugettext_lazy as _
+from . import settings as twostepauth_settings
+from .auth_backend import TwoStepAuthBackend
+
+#from django-profiles
+def get_profile_model():
+    """
+    Return the model class for the currently-active user profile
+    model, as defined by the ``AUTH_PROFILE_MODULE`` setting. If that
+    setting is missing, raise
+    ``django.contrib.auth.models.SiteProfileNotAvailable``.
+
+    """
+    if (not hasattr(settings, 'AUTH_PROFILE_MODULE')) or \
+           (not settings.AUTH_PROFILE_MODULE):
+        raise SiteProfileNotAvailable
+    profile_mod = get_model(*settings.AUTH_PROFILE_MODULE.split('.'))
+    if profile_mod is None:
+        raise SiteProfileNotAvailable
+    return profile_mod
+
+
+def get_profile_form():
+    """
+    Returns a profile model form without the two-step authentication fields
+    """
+    profile_mod = get_profile_model()
+    class _ProfileForm(forms.ModelForm):
+        class Meta:
+            model = profile_mod
+            exclude = ('user', 'tsa_active', 'tsa_secret', 'tsa_backup_codes', 
+                       'tsa_hotp_counter', 'tsa_skew')
+    return _ProfileForm
+
+
+TWOSTEPAUTH_METHOD_OPTIONS = (
+    ('APP', 'Mobile App'),
+    ('BACKUP', 'Backup codes'),
+)
+
+
+class TwoStepAuthenticationForm(AuthenticationForm):
+    """
+    Form for the first step of login in the two-step authentication process.
+    """
+    def __init__(self, *args, **kw):
+        if kw.has_key('remember_token'):
+            self.remember_token = kw['remember_token']
+            del kw['remember_token']
+        else:
+            self.remember_token = None
+        self.user = None
+        self.usertoken = None
+        super(TwoStepAuthenticationForm, self).__init__(*args, **kw)
+
+    def clean(self):
+        """
+        Checks for the username and password.
+
+        If the user cannot be authenticated tries to generate the user token 
+        for the second step of the authentication process. If the token cannot 
+        be generated a validation error is raised.
+
+        If the user is not active a validation error is raised.
+        """
+        username = self.cleaned_data.get('username')
+        password = self.cleaned_data.get('password')
+
+        if username and password:
+            force_single_step = not twostepauth_settings.TWOSTEPAUTH_FOR_USERS
+            self.user = authenticate(username=username, password=password, 
+                            remember_token=self.remember_token, 
+                            force_single_step=force_single_step)
+            if self.user is None:
+                # see if this guy has two-step auth enabled
+                self.usertoken = TwoStepAuthBackend().first_step(
+                                        username=username, password=password)
+                if self.usertoken is None:
+                    raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
+            elif not self.user.is_active:
+                raise forms.ValidationError(_("This account is inactive."))
+        self.check_for_test_cookie()
+        return self.cleaned_data
+
+    def get_user(self):
+        """
+        Returns the authenticated user object if authentication was done, 
+        otherwise it will be None
+        """
+        return self.user
+
+
+class TokenAuthenticationForm(forms.Form):
+    """
+    Form for the second step of login in the two-step authentication process
+    where the application code or backup codes are entered to complete the
+    authentication.
+    """
+    code = forms.IntegerField(label=_("Authentication Code"),
+        help_text=_(u"If you have enabled two-factor authentication, enter the "
+            "six-digit number from your authentication device here."),
+        widget=forms.TextInput(attrs={'maxlength':'8'}),
+        min_value=0, max_value=99999999,
+        required=True
+    )
+    method = forms.CharField(label=_("Authentication method"),
+                             widget=forms.Select(choices = TWOSTEPAUTH_METHOD_OPTIONS),
+                             required = True,
+                            )
+    remember_computer = forms.BooleanField(widget=forms.CheckboxInput(),
+                               required=False,
+                               initial=False,
+                               label=_(u'Remember this computer for %(days)s days') % {'days': twostepauth_settings.TWOSTEPAUTH_REMEMBER_COMPUTER_DAYS})
+    authkey = forms.CharField(widget=forms.HiddenInput())
+    username = forms.CharField(widget=forms.HiddenInput())
+    remember = forms.BooleanField(widget=forms.HiddenInput(), required=False)
+
+    def clean(self):
+        code = self.cleaned_data.get('code')
+        method = self.cleaned_data.get('method')
+        username = self.cleaned_data.get('username')
+        authkey = self.cleaned_data.get('authkey')
+
+        if method == 'APP' and code > 999999:
+            raise forms.ValidationError(_(u"Wrong authentication code format"))
+
+        self.user = authenticate(username=username, code=code, method=method, token=authkey)
+        if not self.user:
+            raise forms.ValidationError(_(u"User authentication failed"))
+        
+        return self.cleaned_data
+
+    def get_user(self):
+        """
+        Returns the authenticated user object if authentication was done, 
+        otherwise it will be None
+        """
+        return self.user
+
+
+class TwoStepAuthEditForm(forms.Form):
+    """
+    Form for use in the user profile to activate/inactivate the use of two-step
+    authentication.
+    """
+    tsa_active = forms.BooleanField(widget=forms.CheckboxInput(),
+                                    required=False,
+                                    label=_(u'Two-Step Authentication'))
+
+
+class SingleStepAdminAuthenticationForm(AdminAuthenticationForm):
+    """
+    Form for authentication in the admin section of the site when the 
+    two-step auth is disabled by the ``TWOSTEPAUTH_FOR_ADMIN`` setting.
+    """
+    def clean(self):
+        username = self.cleaned_data.get('username')
+        password = self.cleaned_data.get('password')
+        message = _("Please enter a correct username and password. "
+                    "Note that username and password fields are case-sensitive.")
+        if username and password:
+            try:
+                user = User.objects.get(username=username)
+            except (User.DoesNotExist, User.MultipleObjectsReturned):
+                raise forms.ValidationError(message)
+            self.user_cache = authenticate(username=username, password=password, 
+                                           force_single_step=True)
+            if self.user_cache is None:
+                raise forms.ValidationError(message)
+            elif not self.user_cache.is_active or not self.user_cache.is_staff:
+                raise forms.ValidationError(message)
+        self.check_for_test_cookie()
+        return self.cleaned_data
+
+
+class TwoStepAdminAuthenticationForm(AdminAuthenticationForm):
+    """
+    Form forauthentication in the admin section of the site when the 
+    two-step auth is enabled by the ``TWOSTEPAUTH_FOR_ADMIN`` setting.
+    """
+    code = forms.IntegerField(label=_("Authentication Code"),
+        help_text=_(u"If you have enabled two-factor authentication, enter the "
+            "six-digit number from your authentication device here."),
+        widget=forms.TextInput(attrs={'maxlength':'8'}),
+        min_value=0, max_value=99999999,
+        required=False
+    )
+    method = forms.CharField(label=_("Authentication method"),
+                             widget=forms.Select(choices = TWOSTEPAUTH_METHOD_OPTIONS),
+                             required = True,
+                            )
+    remember_computer = forms.BooleanField(widget=forms.CheckboxInput(),
+                               required=False,
+                               initial=True,
+                               label=_(u'Remember this computer for %(days)s days') % {
+                                'days': twostepauth_settings.TWOSTEPAUTH_REMEMBER_COMPUTER_DAYS})
+
+    def clean(self):
+        username = self.cleaned_data.get('username')
+        password = self.cleaned_data.get('password')
+        code = self.cleaned_data.get('code')
+        method = self.cleaned_data.get('method')
+
+        #app token codes have 6 digits
+        if method == 'APP' and code > 999999:
+            raise forms.ValidationError(_(u"Wrong authentication code format"))
+
+        message = _("Please enter a correct username, password and token. "
+                    "Note that username and password fields are case-sensitive.")
+        
+        if username and password:
+            try:
+                user = User.objects.get(username=username)
+            except (User.DoesNotExist, User.MultipleObjectsReturned):
+                raise forms.ValidationError(message)
+            try:
+                profile = user.get_profile()
+            except:
+                profile = None
+            
+            if profile and profile.tsa_active:
+                self.user_cache = authenticate(username=username, password=password, 
+                                               code=code, method=method)
+            else:
+                self.user_cache = authenticate(username=username, password=password)
+
+            if self.user_cache is None:
+                raise forms.ValidationError(message)
+            elif not self.user_cache.is_active or not self.user_cache.is_staff:
+                raise forms.ValidationError(message)
+        self.check_for_test_cookie()
+        
+        return self.cleaned_data

File twostepauth/management/__init__.py

Empty file added.

File twostepauth/management/commands/__init__.py

Empty file added.

File twostepauth/management/commands/generate_twostepauth_secret.py

+from django.core.management.base import BaseCommand, CommandError
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib.auth.models import SiteProfileNotAvailable
+from django.contrib.auth.models import User
+from ...utils import generate_secret
+
+
+class Command(BaseCommand):
+    args = '<username>'
+    help = 'Activate Two-Step Authentication, generate a secret and store it in the database'
+
+    def handle(self, *args, **options):
+        if not args:
+            raise CommandError("Username missing.")
+        username = args[0]  # FIXME what if no args?
+        try:
+            user = User.objects.get(username=username)
+        except:
+            raise CommandError("No such user.")
+        try:
+            p = user.get_profile()
+        except (ObjectDoesNotExist, SiteProfileNotAvailable):
+            # we do not create a profile because it might have application-dependent
+            # fields we know nothing about
+            raise CommandError("%s does not have a profile.\n" % (username))
+        secret = generate_secret()
+        p.tsa_active = True
+        p.tsa_secret = secret
+        p.save()
+        self.stdout.write("%s's secret: %s\n" % (username, secret))

File twostepauth/models.py

+#coding: utf-8
+import base64
+import hashlib
+import hmac
+import logging
+import struct
+from time import time as now
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from .utils import generate_secret, generate_backup_codes
+from . import settings as twostepauth_settings
+
+
+class TwoStepAuthBaseProfile(models.Model):
+    tsa_active = models.BooleanField(_('Two-step auth active'), default=False)
+    tsa_secret = models.CharField(_('TSA Secret'), max_length=100, blank=True, null=True)  # TODO should be encoded? neeeded len?
+    tsa_backup_codes = models.CharField(_('TSA Backup codes'), max_length=255, blank=True, null=True)
+    tsa_hotp_counter = models.PositiveIntegerField(_('TSA HOTP counter'), default=1)
+    tsa_skew = models.IntegerField(_('TSA skew'), default=0)
+
+    class Meta:
+        abstract = True
+
+    def twostep_auth_enabled(self):
+        """
+        Returns True if two-step authentication is enabled for users, 
+        otherwise returns False
+        """
+        return twostepauth_settings.TWOSTEPAUTH_FOR_USERS
+
+    def validate_totp(self, code):
+        """based on http://code.google.com/p/google-authenticator/source/browse/libpam/pam_google_authenticator.c
+        function check_timebased_code
+
+        and, for the math:
+        http://www.brool.com/index.php/using-google-authenticator-for-your-website
+
+        Returns:
+            False - invalid code
+            True - valid code
+        """
+        try:
+            code = int(code)
+        except TypeError:
+            return False
+        if code < 0 or code >= 1000000:
+            # All time based verification codes are no longer than six digits.
+            raise False
+        # Compute verification codes and compare them with user input
+        tm = int(now() / twostepauth_settings.TWOSTEPAUTH_TIME_STEP_SIZE)
+
+        for i in range(-(twostepauth_settings.TWOSTEPAUTH_TOTP_WINDOW_SIZE - 1) / 2, (twostepauth_settings.TWOSTEPAUTH_TOTP_WINDOW_SIZE + 1) / 2):
+            time_interval = tm + self.tsa_skew + i
+            computed_hash = self.compute_code(time_interval)
+            if computed_hash == code:
+                return self.invalidate_totp_code(time_interval)
+        if twostepauth_settings.TWOSTEPAUTH_AJDUST_SKEW:
+            #The most common failure mode is for the clocks to be insufficiently
+            #synchronized. We can detect this and store a skew value for future
+            #use.
+            skew = None
+            for i in range(twostepauth_settings.TWOSTEPAUTH_SKEW_ADJUST_WINDOW):
+                computed_hash = self.compute_code(tm - i)
+                if computed_hash == code and skew is None:
+                    #Don't short-circuit out of the loop as the obvious difference in
+                    #computation time could be a signal that is valuable to an attacker.
+                    skew = -i
+                computed_hash = self.compute_code(tm + i)
+                if computed_hash == code and skew is None:
+                    skew = i
+            if skew is not None:
+                return self.check_time_skew(tm, skew)
+        return False
+
+    def validate_hotp(self, code):
+        """
+        Verifies HOTP code and updates the hotp_counter field. Saves the
+        object.
+
+         Returns:
+            False - invalid code
+            True - valid code
+        """
+        try:
+            code = int(code)
+        except TypeError:
+            return False
+        if code < 0 or code >= 1000000:
+            return False
+        if self.tsa_hotp_counter < 1:
+            # missing counter for current user in our database
+            return False
+        for i in range(twostepauth_settings.TWOSTEPAUTH_HOTP_WINDOW_SIZE):
+            computed_hash = self.compute_code(self.tsa_hotp_counter + i)
+            if (computed_hash == code):
+                #advance counter to following step
+                self.tsa_hotp_counter += i + 1
+                self.save()
+                return True
+        #We must advance the counter for each hotp login attempt
+        self.tsa_hotp_counter += 1
+        self.save()
+        return False
+
+    def validate(self, code, method):
+        if method == 'APP':
+            if twostepauth_settings.TWOSTEPAUTH_TOTP:
+                return self.validate_totp(code)
+            else:
+                return self.validate_hotp(code)
+        elif method == 'BACKUP':
+            try:
+                try:
+                    icode = int(code)
+                except TypeError:
+                    return False
+                backup_codes = self.get_backup_codes()
+                if icode in backup_codes:
+                    #remove from the backup list so that it can only be used once
+                    backup_codes.remove(icode)
+                    self.set_backup_codes(backup_codes)
+                    self.save()
+                    return True
+                else:
+                    return False
+            except ValueError:
+                return False
+
+    def compute_code(self, tm):
+        b = struct.pack(">Q", tm)  # unsigned long long?
+        secret = base64.b32decode(self.tsa_secret)
+        hm = hmac.new(secret, b, hashlib.sha1).digest()
+        offset = ord(hm[-1]) & 0x0F
+        truncatedHash = struct.unpack(">I", hm[offset:offset + 4])[0]  # unsigned int
+        truncatedHash &= 0x7FFFFFFF
+        truncatedHash %= 1000000
+        return truncatedHash
+
+    def get_backup_codes(self):
+        """
+        Returns the backup codes from the model as a list of values. Internally the 
+        backup codes are stored as a string of semicolon separated values.
+        """
+        if self.tsa_backup_codes:
+            return map(int, self.tsa_backup_codes.split(';'))
+        else:
+            return []
+
+    def set_backup_codes(self, codes):
+        """
+        Stores the backup codes in the model as a string of semicolon separated values
+        """
+        self.tsa_backup_codes = ";".join(map(str, codes))
+
+    def invalidate_totp_code(self, tm):
+        """If the TWOSTEPAUTH_DISALLOW_REUSE option has been set, record the timestamps that have been
+           used to log in successfully and disallow their reuse.
+
+        Returns:
+             True - the timestamp is allowed
+             False - the timestamp is already blocked
+        """
+        if not twostepauth_settings.TWOSTEPAUTH_DISALLOW_REUSE:
+            return True
+        blocked_ts = self.user.blockedtimestamp_set.filter(timestamp=tm)
+        if blocked_ts:
+            #FIXME better log or show to the user?
+            logger = logging.getLogger(__name__)
+            logger.error("Trying to reuse a previously used time-based code. "
+                  "Retry again in 30 seconds. "
+                  "Warning! This might mean, you are currently subject to a "
+                  "man-in-the-middle attack.")
+            return False
+        #If the blocked code is outside of the possible window of timestamps, remove it.
+        q1 = models.Q(timestamp__lte=tm - twostepauth_settings.TWOSTEPAUTH_TOTP_WINDOW_SIZE)
+        q2 = models.Q(timestamp__gte=twostepauth_settings.TWOSTEPAUTH_TOTP_WINDOW_SIZE + tm)
+        purge_ts = self.user.blockedtimestamp_set.filter(q1 | q2)
+        purge_ts.delete()
+
+        # Add timestamp to the blacklist
+        self.user.blockedtimestamp_set.create(timestamp=tm)
+        return True
+
+    def check_time_skew(self, tm, skew):
+        """
+        If the user enters a sequence of TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE codes that are valid
+        for within RESETTING_SKEW_WINDOW of the current time, and he does it in quick succession,
+        we assume that he's the legitimate user but there's a mismatch between the code
+        generator clock and the system clock, so skew is adjusted.
+        """
+        resetting_list = list(self.user.totp_skew_set.all()[:twostepauth_settings.TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE - 1])
+        if resetting_list:
+            # If the user entered an identical code, assume they are just getting
+            # desperate. This doesn't actually provide us with any useful data,
+            # though. Don't change any state and hope the user keeps trying a few
+            # more times.
+            if (tm, skew) == (resetting_list[0].timestamp, resetting_list[0].skew):
+                return None
+        new_rts = ResettingTimeSkew(user=self.user, timestamp=tm, skew=skew)
+        resetting_list.insert(0, new_rts)
+        # Check if we have the required amount of valid entries.
+        if len(resetting_list) == twostepauth_settings.TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE:
+            #Check that we have a consecutive sequence of timestamps with no big
+            #gaps in between. Also check that the time skew stays constant. Allow
+            #a minor amount of fuzziness on all parameters.
+            if not filter(
+                lambda (tm1, tm0): tm1.timestamp <= tm0.timestamp or tm1.timestamp > tm0.timestamp + 2 or
+                    tm0.skew - tm1.skew < -1 or tm0.skew - tm1.skew > 1,
+                zip(resetting_list[:-1], resetting_list[1:])):
+                    # The user entered the required number of valid codes in quick
+                    # succession. Establish a new valid time skew for all future login
+                    # attempts.
+                    avg_skew = sum([tm.skew for tm in resetting_list]) / twostepauth_settings.TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE
+                    self.tsa_skew = avg_skew
+                    self.save()
+                    self.user.totp_skew_set.all().delete()
+                    return True
+        new_rts.save()
+        self.user.totp_skew_set.filter(timestamp__lt=resetting_list[-1].timestamp).delete()
+        return False
+
+    def save(self):
+        if self.tsa_active:
+            if not self.tsa_secret:
+                self.tsa_secret = generate_secret()
+            if not self.tsa_backup_codes:
+                self.set_backup_codes(generate_backup_codes())
+        super(TwoStepAuthBaseProfile, self).save()
+
+    def __unicode__(self):
+        return unicode(self.user)
+
+
+class BlockedTimestamp(models.Model):
+    user = models.ForeignKey("auth.User")
+    timestamp = models.PositiveIntegerField()
+
+    def __unicode__(self):
+        return u"%s, %u" % (unicode(self.user), self.timestamp)
+
+
+class ResettingTimeSkew(models.Model):
+    user = models.ForeignKey("auth.User", related_name="totp_skew_set")
+    timestamp = models.PositiveIntegerField()
+    skew = models.IntegerField()
+
+    def __unicode__(self):
+        return u"%s, %u%+d" % (unicode(self.user), self.timestamp, self.tsa_skew)
+
+    class Meta:
+        ordering = ['-timestamp']

File twostepauth/settings.py

+from django.conf import settings
+
+# Use T(ime-based)OTP. Set to False to use H(mac-based) OTP
+TWOSTEPAUTH_TOTP = getattr(settings, 'TWOSTEPAUTH_TOTP', True)
+
+#record timestamps have been used to log in successfully and disallow their reuse.
+TWOSTEPAUTH_DISALLOW_REUSE = getattr(settings, 'TWOSTEPAUTH_DISALLOW_REUSE', True)
+
+#FIXME - can we have both at the same time or can we have just 1 window_size setting?
+# Window size for counter-based validation
+TWOSTEPAUTH_HOTP_WINDOW_SIZE = getattr(settings, 'TWOSTEPAUTH_HOTP_WINDOW_SIZE', 3)
+
+# Window size for time-based validation
+TWOSTEPAUTH_TOTP_WINDOW_SIZE = getattr(settings, 'TWOSTEPAUTH_TOTP_WINDOW_SIZE', 10)
+
+# Time-step size in seconds for time-based validation
+TWOSTEPAUTH_TIME_STEP_SIZE = getattr(settings, 'TWOSTEPAUTH_TIME_STEP_SIZE', 30)
+
+# Try to compensate for desynchronized clocks
+TWOSTEPAUTH_AJDUST_SKEW = getattr(settings, 'TWOSTEPAUTH_AJDUST_SKEW', True)
+
+# number of sequential timestamps the user must enter for the skew to be adjusted
+TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE = getattr(settings, 'TWOSTEPAUTH_RESETTING_SKEW_SEQUENCE', 3)  # because google says so :-P
+
+# when adjusting the skew parameter, the number of intervals we search for a matching code before and after the current time
+TWOSTEPAUTH_SKEW_ADJUST_WINDOW = getattr(settings, 'TWOSTEPAUTH_SKEW_ADJUST_WINDOW', 25 * 60)
+
+#Activate two step authentication for the site
+TWOSTEPAUTH_FOR_USERS = getattr(settings, 'TWOSTEPAUTH_FOR_USERS', False)
+
+#Activate two step authentication for the admin section of the site
+TWOSTEPAUTH_FOR_ADMIN = getattr(settings, 'TWOSTEPAUTH_FOR_ADMIN', False)
+
+#Configure the number of days before a computer/browser asks again the token
+TWOSTEPAUTH_REMEMBER_COMPUTER_DAYS = getattr(settings,
+                                        'TWOSTEPAUTH_REMEMBER_COMPUTER_DAYS',
+                                        30)
+
+#session identifier for two step auth login 
+TWOSTEPAUTH_SESSION_KEY = getattr(settings, 'TWOSTEPAUTH_SESSION_KEY', '_auth_2step_user_id')

File twostepauth/templates/twostepauth/adminlogin.html

+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block extrastyle %}{% load adminmedia %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/login.css" />{% endblock %}
+
+{% block bodyclass %}login{% endblock %}
+
+{% block nav-global %}{% endblock %}
+
+{% block content_title %}{% endblock %}
+
+{% block breadcrumbs %}{% endblock %}
+
+{% block content %}
+{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}
+<p class="errornote">
+{% blocktrans count form.errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+</p>
+{% endif %}
+
+{% if form.non_field_errors or form.this_is_the_login_form.errors %}
+{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %}
+<p class="errornote">
+    {{ error }}
+</p>
+{% endfor %}
+{% endif %}
+
+<div id="content-main">
+<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
+  <div class="form-row">
+    {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
+    <label for="id_username" class="required">{% trans 'Username:' %}</label> {{ form.username }}
+  </div>
+  <div class="form-row">
+    {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}
+    <label for="id_password" class="required">{% trans 'Password:' %}</label> {{ form.password }}
+    <input type="hidden" name="this_is_the_login_form" value="1" />
+    <input type="hidden" name="next" value="{{ next }}" />
+  </div>
+  <div class="form-row">
+    {% if not form.this_is_the_login_form.errors %}{{ form.code.errors }}{% endif %}
+    <label for="id_code" class="required">{% trans 'Token code:' %}</label> {{ form.code }}
+  </div>
+  <div class="form-row">
+    {% if not form.this_is_the_login_form.errors %}{{ form.method.errors }}{% endif %}
+    <label for="id_method" class="required">{% trans 'Token method:' %}</label> {{ form.method }}
+  </div>
+  <div class="submit-row">
+    <label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" />
+  </div>
+</form>
+
+<script type="text/javascript">
+document.getElementById('id_username').focus()
+</script>
+</div>
+{% endblock %}

File twostepauth/templates/twostepauth/login.html

+{% extends "base.html" %}
+{% load url from future %}
+
+{% block content %}
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action="{% url 'auth_login' %}">
+{% csrf_token %}
+<legend>Login</legend>
+<label for="id_username">{{ form.username.label_tag }}</label>
+{{ form.username }}
+<br>
+<label for="id_password">{{ form.password.label_tag }}</label>
+{{ form.password }}
+<br>
+<div class="submit">
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{{ next }}" />
+</div>		
+</form>
+{% endblock %}

File twostepauth/templates/twostepauth/signin_token_form.html

+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans "Signin Authentication Code" %}{% endblock %}
+
+{% block content %}
+<form action="{% url login_step_two %}" method="post">
+  {% csrf_token %}
+  <fieldset>
+    <legend>{% trans "Authentication Code" %}</legend>
+    {{ form.non_field_errors }}
+    {% for field in form %}
+    {% if field.is_hidden %}
+    	{{ field }}
+	{% else %}
+		
+    	<p>
+    		{{ field.label_tag }}
+    		{{ field }} 
+			{% if field.errors %}<br>{{ field.errors }}{% endif %}
+    	</p>
+    {% endif %}
+    {% endfor %}
+  </fieldset>
+  <div class="submit">
+  <input type="submit" value="{% trans "Signin" %}" />
+  {% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
+  </div>
+</form>
+{% endblock %}

File twostepauth/tests/__init__.py

+#coding: utf-8
+import base64
+import datetime
+import os
+import time
+from urllib import quote
+from django.utils import unittest
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
+from django.db.models import OneToOneField
+import django.test
+from ..utils import build_chart_url, generate_single_backup_code, generate_secret
+from .. import models
+from .. import settings as ts_settings
+from .auth_backend import TwoStepAuthBackendTestCase
+from .base import TwoStepAuthProfileTestCaseBase, otp_data, hotp_data, get_fake_time_fn
+from .forms import (TestTwoStepAdminAuthenticationForm, TestSingleStepAdminAuthenticationForm,
+            TestTokenAuthenticationForm, TestTwoStepAuthenticationForm, 
+            TestTwoStepAuthenticationFormBase)
+from .utils import TestBackupCodes, TestRememberComputerTokenGenerator
+from .views import TestTwoStepProfile, TestLoginStepOne, TestLoginStepTwo
+
+
+# To define a model that is only present in the database when testing we can do it here
+class TestProfile(models.TwoStepAuthBaseProfile):
+    user = OneToOneField('auth.User')
+    pass
+
+
+class UtilsTestCaseBase(unittest.TestCase):
+    def setUp(self):
+        self.secret = "2SH3V3GDW7ZNMGYE"
+        self.username = 'testuser'
+        self.hostname = 'www.example.com'
+
+    def get_expected_url(self, username, hostname):
+        return 'https://chart.googleapis.com/chart?chl=otpauth%%3A%%2F%%2F%cotp%%2F%s%%40%s%%3Fsecret%%3D%s&chs=200x200&cht=qr&chld=M%%7C0' % (self.otp, username, hostname, self.secret)
+
+
+# Assumes TWOSTEPAUTH_TOTP = True in settings
+class UtilsTestCase(UtilsTestCaseBase):
+    otp = 't'
+
+    def test_build_chart_url(self):
+        """test"""
+        url = build_chart_url(self.secret, self.username, self.hostname)
+        expected_url = self.get_expected_url(self.username, self.hostname)
+        self.assertEqual(url, expected_url)
+
+    def test_generate_single_backup_code(self):
+        numbers = map(str, range(10))
+        backup_code = generate_single_backup_code()
+        self.assertEqual(len(backup_code), 8)
+        for n in backup_code:
+            self.assertTrue(n in numbers)
+        self.assertNotEqual(backup_code[0], '0')
+
+    def test_generate_secret(self):
+        secret = generate_secret()
+        decoded_secret = base64.b32decode(secret)
+        self.assertEqual(len(decoded_secret), 10)
+
+
+class HotpUtilsTestCase(UtilsTestCaseBase):
+    otp = 'h'
+
+    def setUp(self):
+        super(HotpUtilsTestCase, self).setUp()
+        self.original_totp_setting = ts_settings.TWOSTEPAUTH_TOTP
+        ts_settings.TWOSTEPAUTH_TOTP = False
+
+    def tearDown(self):
+        ts_settings.TWOSTEPAUTH_TOTP = self.original_totp_setting
+
+    def test_build_chart_url(self):
+        """test"""
+        url = build_chart_url(self.secret, self.username, self.hostname)
+        expected_url = self.get_expected_url(self.username, self.hostname)
+        self.assertEqual(url, expected_url)
+
+
+class TwoStepAuthProfileTestCase(TwoStepAuthProfileTestCaseBase):
+    fixtures = ['test_users.json', ]
+
+    def setUp(self):
+        super(TwoStepAuthProfileTestCase, self).setUp()
+        self.old_now = models.now
+        models.now = get_fake_time_fn()
+        self.user_otp = User.objects.get(username='test_otp')
+        self.profile = self.user_otp.get_profile()
+        self.original_reuse_setting = ts_settings.TWOSTEPAUTH_DISALLOW_REUSE
+        ts_settings.TWOSTEPAUTH_DISALLOW_REUSE = True
+
+    def tearDown(self):
+        super(TwoStepAuthProfileTestCase, self).tearDown()
+        ts_settings.TWOSTEPAUTH_DISALLOW_REUSE = self.original_reuse_setting
+        models.now = self.old_now
+
+    def test_validate_totp(self):
+        """ test that totp validation works. """
+        self.assertTrue(self.profile.validate_totp(otp_data[0][1]))
+
+    def test_validate_totp_wrong_code(self):
+        """ test that totp validation fails with an invalid code. """
+        self.assertFalse(self.profile.validate_totp('123456'))
+
+    def test_validate_totp_window(self):
+        """ Code OK not for the current time, but for a time inside the window. """
+        self.assertTrue(self.profile.validate_totp(otp_data[2][1]))
+
+    def test_validate_totp_window(self):
+        """ Code OK, but for a time outside the window should fail. """
+        self.assertFalse(self.profile.validate_totp(otp_data[9][1]))
+
+    def test_validate_totp_reuse(self):
+        """ Test that we cannot reuse the same code. """
+        self.assertTrue(self.profile.validate_totp(otp_data[0][1]))
+        self.assertFalse(self.profile.validate_totp(otp_data[0][1]))
+
+    def test_validate_totp_skew(self):
+        """Test skew parameter to adjust for out of sync clocks"""
+        self.assertFalse(self.profile.validate_totp(otp_data[7][1]))
+        self.assertFalse(self.profile.validate_totp(otp_data[8][1]))
+        # After 3 consecutive codes 
+        self.assertTrue(self.profile.validate_totp(otp_data[9][1]))
+        # skew = (otp_data[9]/TWOSTEPAUTH_TIME_STEP_SIZE)-(otp_data[2])/TWOSTEPAUTH_TIME_STEP_SIZE)
+        self.assertEquals(self.profile.tsa_skew, 7)
+
+    def test_validate_hotp(self):
+        """ Hmac-based code OK """
+        self.assertTrue(self.profile.validate_hotp(hotp_data[0][1]))
+
+    def test_validate_hotp_wrong_code(self):
+        """ Invalid Hmac-based code """
+        self.assertFalse(self.profile.validate_hotp('654321'))
+
+    def test_validate_hotp_window(self):
+        """ Hmac-based code OK not for the current time, but for time inside the window """
+        self.assertTrue(self.profile.validate_hotp(hotp_data[2][1]))
+
+    def test_validate_hotp_window(self):
+        """Code OK, but for time outside window"""
+        self.assertFalse(self.profile.validate_hotp(hotp_data[5][1]))
+
+    def test_validate_hotp_window_adjust(self):
+        """ Test that the HOTP window is properly moved after each try.
+        Assumes TWOSTEPAUTH_HOTP_WINDOW_SIZE = 3
+        """
+        # initially internal hotp counter = 1 ; window = (1, 2, 3)
+        # hotp_data[3] has counter == 4, outside of window
+        self.assertFalse(self.user_otp.get_profile().validate_hotp(hotp_data[3][1]))
+        # now the internal counter is 2, so the window should be (2, 3, 4)
+        self.assertTrue(self.user_otp.get_profile().validate_hotp(hotp_data[3][1]))
+        # now the internal counter is 4+1=5, because 4 was the one that matched previously
+        # so the window should be (5, 6, 7) [instead of (3, 4, 5)]
+        # hotp_data[5] has counter == 6, inside the window
+        self.assertTrue(self.user_otp.get_profile().validate_hotp(hotp_data[5][1]))
+        # counter is now 6+1=7
+        self.assertEquals(self.user_otp.get_profile().tsa_hotp_counter, 7)
+
+    def test_validate_hotp_counter_increase(self):
+        """Test that the HOTP counter is properly moved after each try."""