Source code for django_otp.plugins.otp_hotp.models
from base64 import b32encode
from binascii import unhexlify
from urllib.parse import quote, urlencode
from django.conf import settings
from django.db import models
from django_otp.models import Device, ThrottlingMixin
from django_otp.oath import hotp
from django_otp.util import hex_validator, random_hex
def default_key():
return random_hex(20)
def key_validator(value):
return hex_validator()(value)
[docs]
class HOTPDevice(ThrottlingMixin, 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):
verify_allowed, _ = self.verify_is_allowed()
if not verify_allowed:
return False
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.throttle_reset(commit=False)
self.save()
break
else:
verified = False
if not verified:
self.throttle_increment(commit=True)
return verified
[docs]
def get_throttle_factor(self):
return getattr(settings, 'OTP_HOTP_THROTTLE_FACTOR', 1)
@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,
}
urlencoded_params = urlencode(params)
issuer = getattr(settings, 'OTP_HOTP_ISSUER', None)
if callable(issuer):
issuer = issuer(self)
if isinstance(issuer, str) and (issuer != ''):
issuer = issuer.replace(':', '')
label = '{}:{}'.format(issuer, label)
urlencoded_params += '&issuer={}'.format(
quote(issuer)
) # encode issuer as per RFC 3986, not quote_plus
url = 'otpauth://hotp/{}?{}'.format(quote(label), urlencoded_params)
return url