forked from jazzband/django-two-factor-auth
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This relates to django-two-factor-auth jazzband#233, jazzband#215 (and by extension jazzband#86 and others) In a broken state, I'll re visit another time (this was really just to try some ideas)
- Loading branch information
Showing
12 changed files
with
333 additions
and
285 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from django import forms | ||
from django_otp.oath import totp | ||
from django_otp.plugins.otp_totp.models import TOTPDevice | ||
|
||
class TOTPDeviceForm(forms.Form): | ||
token = forms.IntegerField(label=_("Token"), min_value=0, max_value=int('9' * totp_digits())) | ||
|
||
error_messages = { | ||
'invalid_token': _('Entered token is not valid.'), | ||
} | ||
|
||
def __init__(self, key, user, metadata=None, **kwargs): | ||
super(TOTPDeviceForm, self).__init__(**kwargs) | ||
self.key = key | ||
self.tolerance = 1 | ||
self.t0 = 0 | ||
self.step = 30 | ||
self.drift = 0 | ||
self.digits = totp_digits() | ||
self.user = user | ||
self.metadata = metadata or {} | ||
|
||
@property | ||
def bin_key(self): | ||
""" | ||
The secret key as a binary string. | ||
""" | ||
return unhexlify(self.key.encode()) | ||
|
||
def clean_token(self): | ||
token = self.cleaned_data.get('token') | ||
validated = False | ||
t0s = [self.t0] | ||
key = self.bin_key | ||
if 'valid_t0' in self.metadata: | ||
t0s.append(int(time()) - self.metadata['valid_t0']) | ||
for t0 in t0s: | ||
for offset in range(-self.tolerance, self.tolerance): | ||
if totp(key, self.step, t0, self.digits, self.drift + offset) == token: | ||
self.drift = offset | ||
self.metadata['valid_t0'] = int(time()) - t0 | ||
validated = True | ||
if not validated: | ||
raise forms.ValidationError(self.error_messages['invalid_token']) | ||
return token | ||
|
||
def save(self): | ||
return TOTPDevice.objects.create(user=self.user, key=self.key, | ||
tolerance=self.tolerance, t0=self.t0, | ||
step=self.step, drift=self.drift, | ||
digits=self.digits, | ||
name='default') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
|
||
import logging | ||
logger = logging.getLogger(__name__) | ||
|
||
def get_available_methods(): | ||
return [('generator', _('Token generator'))] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from django.forms import Form, ModelForm | ||
from django import forms | ||
|
||
from .models import ( | ||
PhoneDevice, get_available_phone_methods, | ||
) | ||
from .validators import validate_international_phonenumber | ||
|
||
class PhoneNumberMethodForm(ModelForm): | ||
number = forms.CharField(label=_("Phone Number"), | ||
validators=[validate_international_phonenumber]) | ||
method = forms.ChoiceField(widget=forms.RadioSelect, label=_('Method')) | ||
|
||
class Meta: | ||
model = PhoneDevice | ||
fields = 'number', 'method', | ||
|
||
def __init__(self, **kwargs): | ||
super(PhoneNumberMethodForm, self).__init__(**kwargs) | ||
self.fields['method'].choices = get_available_phone_methods() | ||
|
||
|
||
class PhoneNumberForm(ModelForm): | ||
# Cannot use PhoneNumberField, as it produces a PhoneNumber object, which cannot be serialized. | ||
number = forms.CharField(label=_("Phone Number"), | ||
validators=[validate_international_phonenumber]) | ||
|
||
class Meta: | ||
model = PhoneDevice | ||
fields = 'number', | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
from __future__ import absolute_import, division, unicode_literals | ||
|
||
from binascii import unhexlify | ||
|
||
from django.conf import settings | ||
from django.db import models | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
from django_otp.oath import totp | ||
from django_otp.models import Device | ||
from django_otp.util import random_hex | ||
|
||
from phonenumber_field.modelfields import PhoneNumberField | ||
|
||
from .gateways import make_call, send_sms | ||
|
||
import logging | ||
logger = logging.getLogger(__name__) | ||
|
||
PHONE_METHODS = ( | ||
('call', _('Phone Call')), | ||
('sms', _('Text Message')), | ||
) | ||
|
||
|
||
def get_available_methods(): | ||
methods = [] | ||
if getattr(settings, 'TWO_FACTOR_CALL_GATEWAY', None): | ||
methods.append(('call', _('Phone call'))) | ||
if getattr(settings, 'TWO_FACTOR_SMS_GATEWAY', None): | ||
methods.append(('sms', _('Text message'))) | ||
return methods | ||
|
||
|
||
class PhoneDevice(Device): | ||
""" | ||
Model with phone number and token seed linked to a user. | ||
""" | ||
class Meta: | ||
app_label = 'two_factor' | ||
|
||
number = PhoneNumberField() | ||
key = models.CharField(max_length=40, | ||
validators=[key_validator], | ||
default=random_hex, | ||
help_text="Hex-encoded secret key") | ||
method = models.CharField(max_length=4, choices=PHONE_METHODS, | ||
verbose_name=_('method')) | ||
|
||
def __repr__(self): | ||
return '<PhoneDevice(number={!r}, method={!r}>'.format( | ||
self.number, | ||
self.method, | ||
) | ||
|
||
def __eq__(self, other): | ||
if not isinstance(other, PhoneDevice): | ||
return False | ||
return self.number == other.number \ | ||
and self.method == other.method \ | ||
and self.key == other.key | ||
|
||
@property | ||
def bin_key(self): | ||
return unhexlify(self.key.encode()) | ||
|
||
def verify_token(self, token): | ||
# local import to avoid circular import | ||
from two_factor.utils import totp_digits | ||
|
||
try: | ||
token = int(token) | ||
except ValueError: | ||
return False | ||
|
||
for drift in range(-5, 1): | ||
if totp(self.bin_key, drift=drift, digits=totp_digits()) == token: | ||
return True | ||
return False | ||
|
||
def generate_challenge(self): | ||
# local import to avoid circular import | ||
from two_factor.utils import totp_digits | ||
|
||
""" | ||
Sends the current TOTP token to `self.number` using `self.method`. | ||
""" | ||
no_digits = totp_digits() | ||
token = str(totp(self.bin_key, digits=no_digits)).zfill(no_digits) | ||
if self.method == 'call': | ||
make_call(device=self, token=token) | ||
else: | ||
send_sms(device=self, token=token) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
@class_view_decorator(never_cache) | ||
@class_view_decorator(otp_required) | ||
class PhoneSetupView(IdempotentSessionWizardView): | ||
""" | ||
View for configuring a phone number for receiving tokens. | ||
A user can have multiple backup :class:`~two_factor.models.PhoneDevice` | ||
for receiving OTP tokens. If the primary phone number is not available, as | ||
the battery might have drained or the phone is lost, these backup phone | ||
numbers can be used for verification. | ||
""" | ||
template_name = 'two_factor/core/phone_register.html' | ||
success_url = settings.LOGIN_REDIRECT_URL | ||
form_list = ( | ||
('setup', PhoneNumberMethodForm), | ||
('validation', DeviceValidationForm), | ||
) | ||
key_name = 'key' | ||
|
||
def get(self, request, *args, **kwargs): | ||
""" | ||
Start the setup wizard. Redirect if no phone methods available. | ||
""" | ||
if not get_available_phone_methods(): | ||
return redirect(self.success_url) | ||
return super(PhoneSetupView, self).get(request, *args, **kwargs) | ||
|
||
def done(self, form_list, **kwargs): | ||
""" | ||
Store the device and redirect to profile page. | ||
""" | ||
self.get_device(user=self.request.user, name='backup').save() | ||
return redirect(self.success_url) | ||
|
||
def render_next_step(self, form, **kwargs): | ||
""" | ||
In the validation step, ask the device to generate a challenge. | ||
""" | ||
next_step = self.steps.next | ||
if next_step == 'validation': | ||
self.get_device().generate_challenge() | ||
return super(PhoneSetupView, self).render_next_step(form, **kwargs) | ||
|
||
def get_form_kwargs(self, step=None): | ||
""" | ||
Provide the device to the DeviceValidationForm. | ||
""" | ||
if step == 'validation': | ||
return {'device': self.get_device()} | ||
return {} | ||
|
||
def get_device(self, **kwargs): | ||
""" | ||
Uses the data from the setup step and generated key to recreate device. | ||
""" | ||
kwargs = kwargs or {} | ||
kwargs.update(self.storage.validated_step_data.get('setup', {})) | ||
return PhoneDevice(key=self.get_key(), **kwargs) | ||
|
||
def get_key(self): | ||
""" | ||
The key is preserved between steps and stored as ascii in the session. | ||
""" | ||
if self.key_name not in self.storage.extra_data: | ||
key = random_hex(20).decode('ascii') | ||
self.storage.extra_data[self.key_name] = key | ||
return self.storage.extra_data[self.key_name] | ||
|
||
def get_context_data(self, form, **kwargs): | ||
kwargs.setdefault('cancel_url', resolve_url(self.success_url)) | ||
return super(PhoneSetupView, self).get_context_data(form, **kwargs) | ||
|
||
|
||
@class_view_decorator(never_cache) | ||
@class_view_decorator(otp_required) | ||
class PhoneDeleteView(DeleteView): | ||
""" | ||
View for removing a phone number used for verification. | ||
""" | ||
success_url = settings.LOGIN_REDIRECT_URL | ||
|
||
def get_queryset(self): | ||
return self.request.user.phonedevice_set.filter(name='backup') | ||
|
||
def get_success_url(self): | ||
return resolve_url(self.success_url) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
class BackupTokenForm(AuthenticationTokenForm): | ||
otp_token = forms.CharField(label=_("Token")) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from django import forms | ||
|
||
from two_factor.forms import DeviceValidationForm | ||
|
||
try: | ||
from otp_yubikey.models import RemoteYubikeyDevice, YubikeyDevice | ||
except ImportError: | ||
RemoteYubikeyDevice = YubikeyDevice = None | ||
|
||
class YubiKeyDeviceForm(DeviceValidationForm): | ||
token = forms.CharField(label=_("YubiKey")) | ||
|
||
error_messages = { | ||
'invalid_token': _("The YubiKey could not be verified."), | ||
} | ||
|
||
def clean_token(self): | ||
self.device.public_id = self.cleaned_data['token'][:-32] | ||
return super(YubiKeyDeviceForm, self).clean_token() | ||
|
||
|
||
# TODO: Decide if AuthenticationTokenForm should be re produced here (it has | ||
# YubiKey specific handling built in) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from __future__ import absolute_import, division, unicode_literals | ||
|
||
from django.conf import settings | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
import logging | ||
logger = logging.getLogger(__name__) | ||
|
||
try: | ||
import yubiotp | ||
except ImportError: | ||
yubiotp = None | ||
|
||
|
||
def get_available_methods(): | ||
methods = [] | ||
if yubiotp and 'otp_yubikey' in settings.INSTALLED_APPS: | ||
methods.append(('yubikey', _('YubiKey'))) | ||
return methods | ||
|
||
|
Oops, something went wrong.