Source code for django_otp.plugins.otp_hotp.models
from __future__ import absolute_import, division, print_function, unicode_literals
from base64 import b32encode
from binascii import unhexlify
from django.conf import settings
from django.db import models
from django.utils.encoding import force_text
from django.utils.six import string_types
from django.utils.six.moves.urllib.parse import quote, urlencode
from django_otp.models import Device
from django_otp.oath import hotp
from django_otp.util import random_hex, hex_validator
def default_key():
return force_text(random_hex(20))
def key_validator(value):
return hex_validator()(value)
[docs]class HOTPDevice(Device):
"""
A generic HOTP :class:`~django_otp.models.Device`. The model fields mostly
correspond to the arguments to :func:`django_otp.oath.hotp`. They all have
sensible defaults, including the key, which is randomly generated.
.. attribute:: key
*CharField*: A hex-encoded secret key of up to 40 bytes. (Default: 20
random bytes)
.. attribute:: digits
*PositiveSmallIntegerField*: The number of digits to expect from the
token generator (6 or 8). (Default: 6)
.. attribute:: tolerance
*PositiveSmallIntegerField*: The number of missed tokens to tolerate.
(Default: 5)
.. attribute:: counter
*BigIntegerField*: The next counter value to expect. (Initial: 0)
"""
key = models.CharField(max_length=80, validators=[key_validator], default=default_key, help_text="A hex-encoded secret key of up to 40 bytes.")
digits = models.PositiveSmallIntegerField(choices=[(6, 6), (8, 8)], default=6, help_text="The number of digits to expect in a token.")
tolerance = models.PositiveSmallIntegerField(default=5, help_text="The number of missed tokens to tolerate.")
counter = models.BigIntegerField(default=0, help_text="The next counter value to expect.")
class Meta(Device.Meta):
verbose_name = "HOTP device"
@property
def bin_key(self):
"""
The secret key as a binary string.
"""
return unhexlify(self.key.encode())
[docs] def verify_token(self, token):
try:
token = int(token)
except Exception:
verified = False
else:
key = self.bin_key
for counter in range(self.counter, self.counter + self.tolerance + 1):
if hotp(key, counter, self.digits) == token:
verified = True
self.counter = counter + 1
self.save()
break
else:
verified = False
return verified
@property
def config_url(self):
"""
A URL for configuring Google Authenticator or similar.
See https://github.com/google/google-authenticator/wiki/Key-Uri-Format.
The issuer is taken from :setting:`OTP_HOTP_ISSUER`, if available.
"""
label = self.user.get_username()
params = {
'secret': b32encode(self.bin_key),
'algorithm': 'SHA1',
'digits': self.digits,
'counter': self.counter,
}
issuer = getattr(settings, 'OTP_HOTP_ISSUER', None)
if isinstance(issuer, string_types) and (issuer != ''):
issuer = issuer.replace(':', '')
params['issuer'] = issuer
label = '{}:{}'.format(issuer, label)
url = 'otpauth://hotp/{}?{}'.format(quote(label), urlencode(params))
return url