Skip to content

Commit

Permalink
Save the proof of concept, for posterity
Browse files Browse the repository at this point in the history
Don't use this code it's horrid and insecure. It contains at least three
XSS vectors
  • Loading branch information
moreati committed Jul 10, 2015
1 parent e5460d2 commit a44ac23
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
*.py[co]
example/database.sqlite
example/localhost.crt
example/localhost.key
example/settings_private.py
*.egg-info
/.coverage.*
Expand Down
2 changes: 2 additions & 0 deletions example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'django_extensions',
'django_otp',
'django_otp.plugins.otp_static',
'django_otp.plugins.otp_totp',
'otp_yubikey',
'two_factor',
'example',

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'Django>=1.4.2,<1.8.99,!=1.5.*,!=1.6.*',
'django_otp>=0.2.0,<0.2.99',
'qrcode>=4.0.0,<4.99',
'python-u2flib-server',
],
include_package_data=True,
classifiers=[
Expand Down
31 changes: 16 additions & 15 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ envlist =
py27-django14-yubikey,

py27-django17,
py32-django17,
py33-django17,
py34-django17,
# py32-django17,
# py33-django17,
# py34-django17,
py27-django17-custom_user,
py32-django17-custom_user,
py33-django17-custom_user,
py34-django17-custom_user,
# py32-django17-custom_user,
# py33-django17-custom_user,
# py34-django17-custom_user,

py27-django18,
py32-django18,
py33-django18,
py34-django18,
# py32-django18,
# py33-django18,
# py34-django18,
py27-django18-yubikey,
py32-django18-yubikey,
py33-django18-yubikey,
py34-django18-yubikey,
# py32-django18-yubikey,
# py33-django18-yubikey,
# py34-django18-yubikey,
py27-django18-custom_user,
py32-django18-custom_user,
py33-django18-custom_user,
py34-django18-custom_user,
# py32-django18-custom_user,
# py33-django18-custom_user,
# py34-django18-custom_user,

flake8

Expand All @@ -35,6 +35,7 @@ deps =
mock
twilio
qrcode
python-u2flib_server
whitelist_externals = make

; Django 1.4
Expand Down
2 changes: 2 additions & 0 deletions two_factor/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from .utils import monkeypatch_method
from .models import PhoneDevice
from .models import U2FDevice


class AdminSiteOTPRequiredMixin(object):
Expand Down Expand Up @@ -77,3 +78,4 @@ def unpatch_admin():


admin.site.register(PhoneDevice)
admin.site.register(U2FDevice)
64 changes: 64 additions & 0 deletions two_factor/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@
RemoteYubikeyDevice = YubikeyDevice = None

from .models import (PhoneDevice, get_available_phone_methods,
U2FDevice,
get_available_methods)


class MethodForm(forms.Form):
"""
A form to choose a two-factor authentication method (e.g. SMS, OTP).
"""
method = forms.ChoiceField(label=_("Method"),
initial='generator',
widget=forms.RadioSelect)
Expand All @@ -30,6 +34,10 @@ def __init__(self, **kwargs):


class PhoneNumberMethodForm(ModelForm):
"""
A form to provide a phone number, & choose how it's used for
two-factor authentication.
"""
method = forms.ChoiceField(widget=forms.RadioSelect, label=_('Method'))

class Meta:
Expand All @@ -48,6 +56,9 @@ class Meta:


class DeviceValidationForm(forms.Form):
"""
A form that validates the token provided by a new authentication device.
"""
token = forms.IntegerField(label=_("Token"), min_value=1, max_value=int('9' * totp_digits()))

error_messages = {
Expand All @@ -65,7 +76,30 @@ def clean_token(self):
return token


class U2FDeviceForm(DeviceValidationForm):
"""
A form that validates the registration response of a new U2F device.
"""
token = forms.CharField(label=_("U2F device"))

error_messages = {
'invalid_token': _("The U2F response could not be verified."),
}

class Media:
js = ('https://demo.yubico.com/js/u2f-api.js',)

def clean_token(self):
response = self.cleaned_data['token']
if not self.device.verify_registration(response):
raise forms.ValidationError(self.error_messages['invalid_token'])
return response


class YubiKeyDeviceForm(DeviceValidationForm):
"""
A form that validates the OTP token of a new Yubikey device.
"""
token = forms.CharField(label=_("YubiKey"))

error_messages = {
Expand All @@ -78,6 +112,10 @@ def clean_token(self):


class TOTPDeviceForm(forms.Form):
"""
A form that verifies a Time-based One Time Password (TOTP) token,
using a secret key.
"""
token = forms.IntegerField(label=_("Token"), min_value=0, max_value=int('9' * totp_digits()))

error_messages = {
Expand Down Expand Up @@ -128,13 +166,23 @@ def save(self):


class DisableForm(forms.Form):
"""
A form for confirming that two-factor authentication should be disabled.
"""
understand = forms.BooleanField(label=_("Yes, I am sure"))


class AuthenticationTokenForm(OTPAuthenticationFormMixin, Form):
"""
A form for authenticating users with their token.
Usually a second authentication factor, in addition to a password.
"""
otp_token = forms.IntegerField(label=_("Token"), min_value=1,
max_value=int('9' * totp_digits()))

class Media:
js = ('https://demo.yubico.com/js/u2f-api.js',)

def __init__(self, user, initial_device, **kwargs):
"""
`initial_device` is either the user's default device, or the backup
Expand All @@ -152,10 +200,26 @@ def __init__(self, user, initial_device, **kwargs):
isinstance(initial_device, (RemoteYubikeyDevice, YubikeyDevice)):
self.fields['otp_token'] = forms.CharField(label=_('YubiKey'))

elif isinstance(initial_device, U2FDevice):
self.fields['otp_token'] = forms.CharField(label=_('U2F device'))

def clean(self):
self.clean_otp(self.user)
return self.cleaned_data


class BackupTokenForm(AuthenticationTokenForm):
"""
A form for authenticating users with a backup device/token.
Use of a backup token might imply a security problem, e.g.
- the user's other token(s) have been lost or stolen
- the user's other token(s) have been compromised
- an attacker is attempting to bypass the user's other token(s)
Alternatively use of a backup token may be routine, e.g.
- the user's other token(s) are have no signal, or battery
- the user is authenticating on a web browser/user agent
that's incompatiable with their other token(s)
"""
otp_token = forms.CharField(label=_("Token"))
35 changes: 35 additions & 0 deletions two_factor/migrations/0003_u2fdevice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('two_factor', '0002_auto_20150110_0810'),
]

operations = [
migrations.CreateModel(
name='U2FDevice',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(help_text=b'The human-readable name of this device.', max_length=64)),
('confirmed', models.BooleanField(default=True, help_text=b'Is this device ready for use?')),
('public_key', models.TextField()),
('key_handle', models.TextField()),
('app_id', models.TextField()),
('counter', models.PositiveIntegerField(default=0, help_text=b'The non-volatile login counter most recently used by this device.')),
('challenge', models.TextField()),
('last_used_at', models.DateTimeField(null=True)),
('user', models.ForeignKey(help_text=b'The user that this device belongs to.', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
'verbose_name': 'U2F device',
},
),
]
81 changes: 81 additions & 0 deletions two_factor/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from binascii import unhexlify
import json
import logging

from django.conf import settings
Expand All @@ -10,6 +11,8 @@
from django_otp.oath import totp
from django_otp.util import hex_validator, random_hex

from u2flib_server import u2f_v2 as u2f

try:
import yubiotp
except ImportError:
Expand Down Expand Up @@ -49,10 +52,15 @@ def get_available_yubikey_methods():
return methods


def get_available_u2f_methods():
return [('u2f', _('U2F'))]


def get_available_methods():
methods = [('generator', _('Token generator'))]
methods.extend(get_available_phone_methods())
methods.extend(get_available_yubikey_methods())
methods.extend(get_available_u2f_methods())
return methods


Expand Down Expand Up @@ -119,3 +127,76 @@ def generate_challenge(self):
make_call(device=self, token=token)
else:
send_sms(device=self, token=token)


class U2FDevice(Device):
"""
Represents a U2F device
:class:`~django_otp.models.Device`.
"""
public_key = models.TextField()
key_handle = models.TextField()
app_id = models.TextField()

counter = models.PositiveIntegerField(
default=0,
help_text="The non-volatile login counter most recently used by this device."
)

challenge = models.TextField()

class Meta(Device.Meta):
verbose_name = "U2F device"

def to_json(self):
return {
'publicKey': self.public_key,
'keyHandle': self.key_handle,
'appId': self.app_id,
}

def generate_registration(self):
if self.key_handle:
raise RuntimeError("Why are you trying to register this device again?")
challenge = u2f.start_register(self.app_id)
self.challenge = challenge
#self.save()
return "Activate your U2F device to complete registration"

def verify_registration(self, token):
challenge = self.challenge
device, attestation_cert = u2f.complete_register(challenge, token)

self.key_handle = device['keyHandle']
self.public_key = device['publicKey']
self.app_id = device['appId']
self.challenge = ''
return True

def generate_challenge(self):
sign_request = u2f.start_authenticate(self.to_json())
self.challenge = json.dumps(sign_request)
self.save()
return "Activate your U2F device to authenticate"

def verify_token(self, token):
registration = self.to_json()
challenge = self.challenge
response = token
try:
counter, touch_asserted = u2f.verify_authenticate(
registration, challenge, response)
except SystemExit:
print repr(token)
logger.exception('foo')
return False

if counter <= self.counter:
# Could indicate an attack, e.g. the device has been cloned
return False

self.counter = counter
self.challenge = ''
self.save()

return True
17 changes: 17 additions & 0 deletions two_factor/templates/two_factor/core/login.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n two_factor %}

{% block head %}
{{ wizard.form.media }}
{% endblock %}

{% block content %}
<h1>{% block title %}{% trans "Login" %}{% endblock %}</h1>
{{ wizard.form.media }}

{% if wizard.steps.current == 'auth' %}
<p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
Expand All @@ -13,6 +18,18 @@ <h1>{% block title %}{% trans "Login" %}{% endblock %}</h1>
{% elif device.method == 'sms' %}
<p>{% blocktrans %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% elif device.challenge %}
<script>
var u2f_register_challenge = {{ device.challenge|safe }};
u2f.sign([u2f_register_challenge], function(resp) {
var input = document.getElementById("id_token-otp_token");
var form = input.form;
input.value = JSON.stringify(resp);
if ( true ) {
form.submit();
}
});
</script>
{% else %}
<p>{% blocktrans %}Please enter the tokens generated by your token
generator.{% endblocktrans %}</p>
Expand Down

0 comments on commit a44ac23

Please sign in to comment.