Extending Django-OTP

A django-otp plugin is defined as a Django app that includes at least one model derived from django_otp.models.Device. All Device-derived model objects will be detected by the framework and included in the standard forms and APIs.

Writing a Device

A Device subclass is only required to implement one method:

Device.verify_token(token)[source]

Verifies a token.

As a rule, the token should no longer be valid if this returns True.

Parameters:

token (str) – The OTP token provided by the user.

Return type:

bool

Most devices will also need to define one or more model fields to do anything interesting. Here’s a simple implementation of a generic TOTP device:

from binascii import unhexlify

from django.db import models

from django_otp.models import Device
from django_otp.oath import totp
from django_otp.util import random_hex, hex_validator


class TOTPDevice(Device):
    key = models.CharField(max_length=80,
                           validators=[hex_validator()],
                           default=lambda: random_hex(20),
                           help_text='A hex-encoded secret key of up to 40 bytes.')

    @property
    def bin_key(self):
        return unhexlify(self.key)

    def verify_token(self, token):
        """
        Try to verify ``token`` against the current and previous TOTP value.
        """
        try:
            token = int(token)
        except ValueError:
            verified = False
        else:
            verified = any(totp(self.bin_key, drift=drift) == token for drift in [0, -1])

        return verified

This example also shows some of the low-level utilities django_otp provides for OATH and hex-encoded values.

If a device uses a challenge-response algorithm or requires some other kind of user interaction, it should implement an additional method:

Device.generate_challenge()[source]

Generates a challenge value that the user will need to produce a token.

This method is permitted to have side effects, such as transmitting information to the user through some other channel (email or SMS, perhaps). And, of course, some devices may need to commit the challenge to the database.

Returns:

A message to the user. This should be a string that fits comfortably in the template 'OTP Challenge: {0}'. This may return None if this device is not interactive.

Return type:

string or None

Raises:

Any Exception is permitted. Callers should trap Exception and report it to the user.

Helpers

django_otp.models also provides a few mixins and other helpers for common functionality. These are entirely optional, but can be helpful for common functionality.

class django_otp.models.SideChannelDevice(*args, **kwargs)[source]

Abstract base model for a side-channel device attached to a user.

This model implements token generation, verification and expiration, so the concrete devices only have to implement delivery.

token

The token most recently generated for the user.

valid_until

The datetime at which the stored token will expire.

generate_token(length=6, valid_secs=300, commit=True)[source]

Generates a token of the specified length, then sets it on the model and sets the expiration of the token on the model.

Parameters:
  • length (int) – Number of decimal digits in the generated token.

  • valid_secs (int) – Amount of seconds the token should be valid.

  • commit (bool) – Pass False if you intend to save the instance yourself.

verify_token(token)[source]

Verifies a token by content and expiry.

On success, the token is cleared and the device saved.

Parameters:

token (str) – The OTP token provided by the user.

Return type:

bool

class django_otp.models.CooldownMixin(*args, **kwargs)[source]

Mixin class for models requiring a cooldown duration between challenge generations.

Subclass must implement get_cooldown_duration(), and must use the generate_is_allowed() method from within their generate_challenge() method. Further it must use cooldown_set() when a token is generated.

See the implementation of EmailDevice for an example.

last_generated_timestamp

The last time a token was generated for this device.

cooldown_reset(commit=True)[source]

Call this method to reset cooldown (normally after a successful verification).

Parameters:

commit (bool) – Pass False if you intend to save the instance yourself.

cooldown_set(commit=True)[source]

Call this method to set the cooldown timestamp to now (normally when a token is generated).

Parameters:

commit (bool) – Pass False if you intend to save the instance yourself.

generate_is_allowed()[source]

If token generation is allowed, returns (True, None). Otherwise, returns (False, data_dict).

data_dict contains further information. Currently it can be:

{
    'reason': GenerateNotAllowed.COOLDOWN_DURATION_PENDING,
    'next_generation_at': when,
}

where when is a datetime marking the end of the cooldown period. See GenerateNotAllowed.

get_cooldown_duration()[source]

This must be implemented to return the cooldown duration in seconds.

A duration of 0 disables the cooldown.

Normally this is just a wrapper for a plugin-specific setting like OTP_EMAIL_COOLDOWN_DURATION.

class django_otp.models.ThrottlingMixin(*args, **kwargs)[source]

Mixin class for models that want throttling behaviour.

This implements exponential back-off for verifying tokens. Subclasses must implement get_throttle_factor(), and must use the verify_is_allowed(), throttle_reset() and throttle_increment() methods from within their verify_token() method.

See the implementation of EmailDevice for an example.

throttling_failure_timestamp

The datetime of the last failed verification attempt.

throttling_failure_count

The number of consecutive failed verification attempts.

get_throttle_factor()[source]

This must be implemented to return the throttle factor.

The number of seconds required between verification attempts will be \(c2^{n-1}\) where c is this factor and n is the number of previous failures. A factor of 1 translates to delays of 1, 2, 4, 8, etc. seconds. A factor of 0 disables the throttling.

Normally this is just a wrapper for a plugin-specific setting like OTP_EMAIL_THROTTLE_FACTOR.

throttle_increment(commit=True)[source]

Call this method to increase throttling (normally when a verify attempt failed).

Parameters:

commit (bool) – Pass False if you intend to save the instance yourself.

throttle_reset(commit=True)[source]

Call this method to reset throttling (normally when a verify attempt succeeded).

Parameters:

commit (bool) – Pass False if you intend to save the instance yourself.

verify_is_allowed()[source]

If verification is allowed, returns (True, None). Otherwise, returns (False, data_dict).

data_dict contains further information. Currently it can be:

{
    'reason': VerifyNotAllowed.N_FAILED_ATTEMPTS,
    'failure_count': n
}

where n is the number of successive failures. See VerifyNotAllowed.

class django_otp.models.TimestampMixin(*args, **kwargs)[source]

Mixin class that adds timestamps to devices.

This mixin adds fields to record when a device was initially created in the system and when it was last used. It enhances the ability to audit device usage and lifecycle.

Subclasses can use set_last_used_timestamp() to update the last_used_at timestamp whenever the device is used for verification.

created_at

The datetime at which this device was created.

last_used_at

The datetime at which this device was last successfully used for verification.

set_last_used_timestamp(commit=True)[source]

Updates the last_used_at field to the current datetime to indicate that the device has been used.

Parameters:

commit (bool) – Pass False if you intend to save the instance yourself.

Utilities

django_otp provides several low-level utilities as a convenience to plugin implementors.

django_otp.oath

django_otp.oath.hotp(key, counter, digits=6)[source]

Implementation of the HOTP algorithm from RFC 4226.

Parameters:
  • key (bytes) – The shared secret. A 20-byte string is recommended.

  • counter (int) – The password counter.

  • digits (int) – The number of decimal digits to generate.

Returns:

The HOTP token.

Return type:

int

>>> key = b'12345678901234567890'
>>> for c in range(10):
...     hotp(key, c)
755224
287082
359152
969429
338314
254676
287922
162583
399871
520489
django_otp.oath.totp(key, step=30, t0=0, digits=6, drift=0)[source]

Implementation of the TOTP algorithm from RFC 6238.

Parameters:
  • key (bytes) – The shared secret. A 20-byte string is recommended.

  • step (int) – The time step in seconds. The time-based code changes every step seconds.

  • t0 (int) – The Unix time at which to start counting time steps.

  • digits (int) – The number of decimal digits to generate.

  • drift (int) – The number of time steps to add or remove. Delays and clock differences might mean that you have to look back or forward a step or two in order to match a token.

Returns:

The TOTP token.

Return type:

int

>>> key = b'12345678901234567890'
>>> now = int(time())
>>> for delta in range(0, 200, 20):
...     totp(key, t0=(now-delta))
755224
755224
287082
359152
359152
969429
338314
338314
254676
287922
class django_otp.oath.TOTP(key, step=30, t0=0, digits=6, drift=0)[source]

An alternate TOTP interface.

This provides access to intermediate steps of the computation. This is a living object: the return values of t and token will change along with other properties and with the passage of time.

Parameters:
  • key (bytes) – The shared secret. A 20-byte string is recommended.

  • step (int) – The time step in seconds. The time-based code changes every step seconds.

  • t0 (int) – The Unix time at which to start counting time steps.

  • digits (int) – The number of decimal digits to generate.

  • drift (int) – The number of time steps to add or remove. Delays and clock differences might mean that you have to look back or forward a step or two in order to match a token.

>>> key = b'12345678901234567890'
>>> totp = TOTP(key)
>>> totp.time = 0
>>> totp.t()
0
>>> totp.token()
755224
>>> totp.time = 30
>>> totp.t()
1
>>> totp.token()
287082
>>> totp.verify(287082)
True
>>> totp.verify(359152)
False
>>> totp.verify(359152, tolerance=1)
True
>>> totp.drift
1
>>> totp.drift = 0
>>> totp.verify(359152, tolerance=1, min_t=3)
False
>>> totp.drift
0
>>> del totp.time
>>> totp.t0 = int(time()) - 60
>>> totp.t()
2
>>> totp.token()
359152
t()[source]

The computed time step.

property time

The current time.

By default, this returns time.time() each time it is accessed. If you want to generate a token at a specific time, you can set this property to a fixed value instead. Deleting the value returns it to its ‘live’ state.

token()[source]

The computed TOTP token.

verify(token, tolerance=0, min_t=None)[source]

A high-level verification helper.

Parameters:
  • token (int) – The provided token.

  • tolerance (int) – The amount of clock drift you’re willing to accommodate, in steps. We’ll look for the token at t values in [t - tolerance, t + tolerance].

  • min_t (int) – The minimum t value we’ll accept. As a rule, this should be one larger than the largest t value of any previously accepted token.

Return type:

bool

Iff this returns True, self.drift will be updated to reflect the drift value that was necessary to match the token.

django_otp.util

django_otp.util.hex_validator(length=0)[source]

Returns a function to be used as a model validator for a hex-encoded CharField. This is useful for secret keys of all kinds:

def key_validator(value):
    return hex_validator(20)(value)

key = models.CharField(max_length=40, validators=[key_validator], help_text='A hex-encoded 20-byte secret key')
Parameters:

length (int) – If greater than 0, validation will fail unless the decoded value is exactly this number of bytes.

Return type:

function

>>> hex_validator()('0123456789abcdef')
>>> hex_validator(8)(b'0123456789abcdef')
>>> hex_validator()('phlebotinum')          
Traceback (most recent call last):
    ...
ValidationError: ['phlebotinum is not valid hex-encoded data.']
>>> hex_validator(9)('0123456789abcdef')    
Traceback (most recent call last):
    ...
ValidationError: ['0123456789abcdef does not represent exactly 9 bytes.']
django_otp.util.random_hex(length=20)[source]

Returns a string of random bytes encoded as hex.

This uses os.urandom(), so it should be suitable for generating cryptographic keys.

Parameters:

length (int) – The number of (decoded) bytes to return.

Returns:

A string of hex digits.

Return type:

str

django_otp.util.random_number_token(length=6)[source]

Returns a string of random digits encoded as string.

Parameters:

length (int) – The number of digits to return.

Returns:

A string of decimal digits.

Return type:

str