Git Product home page Git Product logo

django-otp's Introduction

django-otp

PyPI

Documentation

Source

This project makes it easy to add support for one-time passwords (OTPs) to Django. It can be integrated at various levels, depending on how much customization is required. It integrates with django.contrib.auth, although it is not a Django authentication backend. The primary target is developers wishing to incorporate OTPs into their Django projects as a form of two-factor authentication.

Several simple OTP plugins are included and more are available separately. This package also includes an implementation of OATH HOTP and TOTP for convenience, as these are standard OTP algorithms used by multiple plugins.

If you're looking for a higher-level or more opinionated solution, you might be interested in django-two-factor-auth.

Status

This project is stable and maintained, but is no longer actively used by the author and is not seeing much ongoing investment.

Well-formed issues and pull requests are welcome, but please see CONTRIBUTING.rst first. General questions and ideas should be directed to the Discussions section; issues should be reserved for confirmed bugs.

Development

This project is built and managed with hatch. If you don't have hatch, I recommend installing it with pipx: pipx install hatch.

pyproject.toml defines several useful scripts for development and testing. The default environment includes all dev and test dependencies for quickly running tests. The test environment defines the test matrix for running the full validation suite. Everything is executed in the context of the Django project in test/test_project.

As a quick primer, hatch scripts can be run with hatch run [<env>:]<script>. To run linters and tests in the default environment, just run hatch run check. This should run tests with your default Python version and the latest Django. Other scripts include:

  • manage: Run a management command via the test project. This can be used to generate migrations.
  • lint: Run all linters.
  • fix: Run all fixers to address linting issues. This may not fix every issue reported by lint.
  • test: Run all tests.
  • check: Run linters and tests.
  • warn: Run tests with all warnings enabled. This is especially useful for seeing deprecation warnings in new versions of Django.
  • cov: Run tests and print a code coverage report.

To run the full test matrix, run hatch run test:run. You will need multiple specific Python versions installed for this.

You can clean up the hatch environments with hatch env prune, for example to force dependency updates.

The project under test can be run as a simple interactive test environment. Run hatch run manage runserver and open it in a browser. This has an implementation of the login form and views with different combinations of decorators, which you can experiment with or use to test changes.

Configuration

By default, the test project uses SQLite. Because SQLite doesn't support row locking, some concurrency tests will be skipped. To test against PostgreSQL, you can add a local configuration file that points to your database.

Configuration is taken from TOML files stored under test/config. A config file named env-<env-name>.toml will be automatically applied when running inside a matching hatch environment. For example, env-default.toml applies to the default development environment and env-test.toml applies to the test matrix environments.

With a wide-open PostgreSQL install, an env-test.toml might look like this:

[database]
ENGINE = "django.db.backends.postgresql"
NAME = "django-otp"
USER = "postgres"

For development, the config file can also be used to inject Django apps and middleware, or to override arbitrary Django settings. See test/config/sample.toml for a full description.

You can also force a specific config file by setting the environment variable DJANGO_OTP_CONFIG to a path.

The Future

Once upon a time, everything was usernames and passwords. Or even in the case of other authentication mechanisms, a user was either authenticated or not (anonymous in Django's terminology). Then there was two-factor authentication, which could simply be an implementation detail in a binary authentication state, but could also imply levels or degrees of authentication.

These days, it's increasingly common to see sites with more nuanced authentication state. A site might remember who you are forever—so you're not anonymous—but if you try to do anything private, you have to re-authenticate. You may be able to choose from among all of the authentication mechanisms you have configured, or only from some of them. Specific mechanisms may be required for specific actions, such as using your U2F device to access your U2F settings.

In short, the world seems to be moving beyond the assumptions that originally informed Django's essential authentication design. If I were still investing in Django generally, I would probably start a new multi-factor authentication project that would reflect these changes. It would incorporate the idea that a user may be authenticated by various combinations of mechanisms at any time and that different combinations may be required to satisfy diverse authorization requirements across the site. It would most likely try to disentangle authentication persistence from sessions, at least to some extent. Many sites would not require all of this flexibility, but it would open up possibilities for better experiences by not asking users for more than we require at any point.

If anyone has a mind to take on a project like this, I'd be happy to offer whatever advice or lessons learned that I can.

django-otp's People

Contributors

11joselu avatar acatton avatar ajmath62 avatar alankokoiev avatar alexsilva avatar bluetech avatar bparmentier avatar cabarnes avatar claudep avatar davidbrai avatar dekkers avatar demestav avatar eschnitzler avatar felixxm avatar filiplajszczak avatar gingrassia avatar gmorell avatar grutt avatar hylje avatar illia-v avatar jaap3 avatar jduar avatar jhillacre avatar kittywaresz avatar lesprimus avatar mbertheau avatar psagers avatar robi-richter avatar spookylukey avatar unchris avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-otp's Issues

Update documentation on how to replace the default admin site with OTPAdminSite

Django has had an "official" way to replace the default admin site, documented here: https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#overriding-the-default-admin-site (Introduced in Django 2.1)

Basically it boils down to adding the following to a project:

from django.contrib.admin.apps import AdminConfig as DefaultAdminConfig


class AdminConfig(DefaultAdminConfig):
    default_site = 'django_otp.admin.OTPAdminSite'

and then replacing ''django.contrib.admin'inINSTALLED_APPS` with the path to this new module.

django-otp could even ship this module itself. That would make swapping the admin site even easier.

I just tried this in a new project and it seems to work just fine.

I'm also wondering if the otpadmin name override on OTPAdminSite is still necessary?

Incorrect number of digits generated

the hotp function sometimes returns incorrect number of digits. Below are some examples where this happens. I set the digits argument to 4, but it returns a token with 3 digits.

>>> hotp(b'\x07\xd4\x9ey\xeeh\x87\xc3I\xac\xfa\xa3VR9\xe1\xf1\x96\x02)', 54, digits=4)
243

>>> hotp(b'\x07\xd4\x9ey\xeeh\x87\xc3I\xac\xfa\xa3VR9\xe1\xf1\x96\x02)', 56, digits=4)
676

>>> hotp(b'\x07\xd4\x9ey\xeeh\x87\xc3I\xac\xfa\xa3VR9\xe1\xf1\x96\x02)', 71, digits=4)
154

>>> hotp(b'\x07\xd4\x9ey\xeeh\x87\xc3I\xac\xfa\xa3VR9\xe1\xf1\x96\x02)', 86, digits=4)
738

Let me know how to fix this.

Making requests too fast removes verification for user and sends him to verification page

Hello, I've implemented the 2fa to the projects fairly easy. I'm using Django 2.2.6 and django-otp 0.7.3 and everything works fine.

I have a monolith web application with server side rendering using Django Templates and Ajax calls for pagination on tables. The backend returns the new table as html and i replace the current table with the new one.

The problem is when i click on the pagination too fast and spam requests, the backend removes the verification from the user and returns a 302 redirect to the verification page. The user remains authenticated, but not verified.

I've looked through the source code and haven't found any setting that removes the verification if requests are coming too fast or a limit on how many request a user can make per second.

I didn't find any resources about this on stackoverflow, any help would be appreciated.

Thank you for the package.

otp_totp plugin username filed as UUID

Hi,
I have my username field as type UUID.
Seems that method config_url in otp_totp/models.py

label = self.user.get_username()

...label ends up being a UUID which does not work for quote method in urllib since it is expecting a string.

Any ideas on how to fix this? Can label be casted to a string here or any other ways of having self.user.getusername() end up being a string somewhere else in my code?

//Thanks

Using BigAutoField as the Default_AUTO_FIELD causes a migration

Starting with Django 3.2 the DEFAULT_AUTO_FIELD setting is django.db.models.BigAutoField
https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys

On a new project using django_otp with the totp plugin causes this when running makemigrations:

Migrations for 'otp_totp':
  /usr/local/lib/python3.9/site-packages/django_otp/plugins/otp_totp/migrations/0003_alter_totpdevice_id.py
    - Alter field id on totpdevice

However, since this is not a user migration, it doesn't go into source control and other stuff.

The solution is to define the type of the Ids in the tables specifically, so for example

from django.db import models

class MyModel(models.Model):
    id = models.BigAutoField(primary_key=True)

BigAutoField is on django since 1.10, so compatibility wouldn't be an issue. The alternative to avoid a migration for something that is probably not going to be needed (Who is going to have more than 2147483647 devices, after all) would be to use models.AutoField instead.

QR scanner can't detect the QR code in the admin dark theme

Starting with Django 3.2, Django now has a native dark theme.

Since the QR image generated doesn't have a background, it results in this;
image

I'm guessing this may change depending on the phone camera or the qr code, but at least my phone with the google authenticator app can't detect the qr code.

According to the documentation of python-qrcode, it supports setting the background color when generating the image, with the back_color parameter, so I think that should fix the issue.

Missing tag for 0.7.4

There is no tag for 0.7.4 but a source package with this version is available on pypi.org.

django_otp/templates/otp/admin14/login.html: 'future' is not a registered tag library

Lately getting the following error:

Invalid template /usr/local/lib/python3.7/site-packages/django_otp/templates/otp/admin14/login.html: 'future' is not a registered tag library. Must be one of:
admin_list
admin_modify
admin_static
admin_urls
... more suggestions ...

Any advice please?

pip freeze | grep -e Django -e django-otp
Django==2.2.7
django-otp==0.7.3

Code never reaches _verify_user method in OTPMiddleware

Hello, I'm having some trouble implementing the OTP flow. I'm using Django rest framework and Django OAuth Toolkit. After verifying a TOTP code with a confirmed device, the fields request.user.otp_device and request.user.is_verified should be set. Using a debugger, I can see that the code never reaches the _verify_user method in OTPMiddleware. Would appreciate all the help I can get with this issue, thanks!

This is some settings I have configured:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django_otp.middleware.OTPMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

'DEFAULT_AUTHENTICATION_CLASSES': (
      'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
  ),
'DEFAULT_PERMISSION_CLASSES': (
    'rest_framework.permissions.IsAuthenticated',
    'auth.permissions.IsVerified',
),

Enable extra widget attributes on login form fields

I'd like to add data-lpignore="true" to the otp_token field on the login form to indicate that LastPass shouldn't save or try to pre-populate this field.

Is there a way currently that I can accomplish this?

Otherwise, it'd be nice to have something where we can add extra attributes to the form field's widget.

Possible circumvention of otp, no logging of potential abuse

Hi!

First off: Thanks for django_otp!

Why this request:
It seems there is a way around django_otp with a 6 digit token. An attacker can just try the same codes (up to 3 every 30 seconds it seems) for months on end. Throttling will not stop this, i've tried this for a few minutes. It seems that given enough time (months), the attacker is able to log in as the OTP will change every 30 seconds :). So you're not brute forcing, but the OTP is doing that for you.

This could be detected if:

  1. wrongly entered codes (the ValidationErrors) are also logged, so operation can notice suspicious behavior. For example including the username or something that indicates which users credentials might have leaked. Signals might also work.
  2. throttling is very useful for quicker attacks, but after tons of wrong codes there should be a block on logging in (that should be a django_two_factor_auth thing).

Logins are already monitored but that might not be enough in busy applications.

Does this make sense? Did i overlook something for these claims?

Token field no longer rendered on login for anonymous users

After upgrading to 0.8.0 (from 0.7.5), the admin login form no longer shows the token field if the user is currently anonymous. It is no longer possible to authenticate and verify in a single form post.

The problem is in the new 1.11 template. The rendering of the otp_token field has now been moved into the {% if form.get_user %} check. Please see the below diff snippet:

   </div>
   {% if form.get_user %}
   <div class="form-row">
-      <label for="id_otp_device">{% trans 'OTP Device:' %}</label> {{ form.otp_device }}
+    {{ form.otp_device.errors }}
+    <label for="id_otp_device">{% trans 'OTP Device:' %}</label> {{ form.otp_device }}
   </div>
-  {% endif %}
   <div class="form-row">
-    {% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %}
+    {{ form.otp_token.errors }}
     <label for="id_otp_token" class="required">{% trans 'OTP Token:' %}</label> {{ form.otp_token }}
   </div>
+  {% endif %}
   {% url 'admin_password_reset' as password_reset_url %}
   {% if password_reset_url %}
   <div class="password-reset-link">

Note that {% endif %} has been moved a few lines down. I believe only this slight change should be reverted.

Cheers and thanks for the great project!

User_logged_in signal not received

I'd like to perform extra stuff after user's successful login but it seems the Django user_logged_in signal is not triggered.

from django.contrib.auth import user_logged_in
from django.dispatch import receiver

@receiver(user_logged_in)
def extra_checks(sender, user, request, **kwargs):
   print("Login signal triggered")

Please let me know if the signal is available. I can try and add it in a pull request if not present.

OTP Expiring Early

for some reason OTP generated is expiring early or expiring before expiring time finishes.it was happening on first time when i create token with the device object.

Issue : i have set expiring time as 300sec and TOTP digit as 4. when a token is generates first time with a device , token is expiring early or expiring before the expiring time finishes. This issue is happening on first time when i create token with the device.

This is my model :

class VerificationDevice(Device):
    otp = models.CharField(max_length=16)
    secret_key = models.CharField(
        max_length=40,
        default=default_key,
        validators=[hex_validator],
        help_text="Hex-encoded secret key to generate totp tokens.",
        unique=True,
    )
    last_verified_counter = models.BigIntegerField(
        default=-1,
        help_text=("The counter value of the latest verified token."
                   "The next token must be at a higher counter value."
                   "It makes sure a token is used only once.")
    )
    verified = models.BooleanField(default=False)

    step = settings.TOTP_TOKEN_VALIDITY
    digits = settings.TOTP_DIGITS

    class Meta(Device.Meta):
        verbose_name = "Verification Device"

    @property
    def bin_key(self):
        return unhexlify(self.secret_key.encode())

    def totp_obj(self):
        totp = TOTP(key=self.bin_key, step=self.step, digits=self.digits)
        totp.time = time.time()
        return totp

    def generate_challenge(self):
        totp = self.totp_obj()
        token = str(totp.token()).zfill(self.digits)
        return token

      def verify_token(self, token, tolerance=0):
        verify_allowed = self.verify_is_allowed()
        if not verify_allowed:
            return False

        totp = self.totp_obj()
        if ((totp.t() > self.last_verified_counter) and
                (totp.verify(token, tolerance=tolerance))):
            self.last_verified_counter = totp.t()
            self.verified = True
            self.save()
            return True
        else:
            return False

View.py

class VerifyOTP(APIView):
    permission_classes = (AllowAny,)
    serializer_class = VerifyOTPSerializer

    def post(self, request):
        serializer = VerifyOTPSerializer(data=request.data, context={'request': request})
        if serializer.is_valid():
            user_id = self.request.session.get('moblie_verify_id', None)
            try:
                device = VerificationDevice.objects.get(
                    user_id=user_id, label='phone_verify')
            except:
                content = {'status': status.HTTP_404_NOT_FOUND, 'message': 'Object not found',
                           'data': serializer.data}
                return Response(content, status=status.HTTP_404_NOT_FOUND)
            user = device.user
            otp = serializer.validated_data['otp']
            with transaction.atomic():
                resp = device.verify_token(otp)
                if resp is True:
                    if user.mobile_number != device.unverified_phone:
                        user.mobile_number = device.unverified_phone
                    user.mobile_number_verified = True
                    user.is_active = True
                    user.is_mobile_number_verified = True
                    user.save()
                    device.delete()
                    self.request.session.pop('mobile_verify_id', None)
                    content = {'status': status.HTTP_200_OK, 'message': 'OTP Verified Successfully',
                               'data': serializer.data}
                    return Response(content, status=status.HTTP_200_OK)
                elif device.verify_token(token=otp, tolerance=5):
                    content = {'status': status.HTTP_406_NOT_ACCEPTABLE, 'message': 'OTP Expired'}
                    return Response(content, status=status.HTTP_406_NOT_ACCEPTABLE)
                else:
                    content = {'status': status.HTTP_406_NOT_ACCEPTABLE, 'message': 'Invalid OTP. Enter a valid OTP'}
                    return Response(content)
        content = {'status': status.HTTP_400_BAD_REQUEST, 'message': serializer.errors}
        return Response(content, status=status.HTTP_200_OK)

settings.py

TOTP_TOKEN_VALIDITY = 300
TOTP_DIGITS = 4

Where i'm doing wrong , did i miss some thing..?

Cannot translate fields

Kindly add verbose_name, ugettext_lazy to all models fields, help_texts, labels, so we get to translate this to other languages.

Thank you,

template error on Django 3.0.x

I am using admin.site.__class__ = OTPAdminSite to set the default admin site class.

Below is the error when using with Django 3.0.x:

'admin_static' is not a registered tag library. Must be one of: admin_list admin_modify admin_urls cache i18n l10n log static tz

{% extends "admin/base_site.html" %}
{% load i18n admin_static %} 
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" />
{{ form.media }}
 
<style type="text/css">
input#id_otp_token,
select#id_otp_device
{

Make QR code white on dark background

The django admin interface seems to respect your operating system's theme of either light mode or dark mode. Currently, the QR code image is impossible to scan if on a dark mode background.

OTPTokenForm - otp_token field error

Hi there,

I had a question question regarding how OTPTokenForm processes token errors. I'm trying to add the correct markup to my form, however I've noticed that token error's are returned as non_field_errors which means the field doesn't have any direct errors. Shouldn't a token error be returned as a problem with the otp_token field?

Not a huge thing, but could be a more standard way of returning the error?

OTP able verify unconfirmed device

Hi,

First of all I would like to say thanks for this great library.
I have an issue when a user have 2 devices, 1 device is confirmed = True and another 1 is confirmed = False.
When user try to login, I will get a confirmed device instead of unconfirmed device and when user using TOTP generated from Google Authenticator from uncofirmed device, the TOTP is valid even it's two different device.
Below is the snippet of my code:

device.verify_token(token)

The "locked_until" value seems incorrect?

Pardon me if I am missing something, but this line should use self.throttling_failure_timestamp instead of current timestamp, right?

The actual throttle check is not currently affected, but the advertised value for locked_until will just keep moving because it is based on current timestamp, and may at one point advertise a time in the future when it really is no longer locked.

How can I get the 6digits token from the device secret key

I'm trying to implement 2fa using this library with restful API.

I'm using the devices_for_user() function to get registered user's devices.
and then trying to send to the user the 6digits token that may be verified with the TOTPDevice.verify_token model method.

but unfortunately can't find a way to generate the right 6digits token for it.

when I'm using the "Google authenticator app" and scan the QR code, I get the right token to be verified.

but, when trying to use django_otp.oatu.totp() for the same reason it won't work for me.

any idea how can I solve it\ what am i doing wrong?

THANKS!

Version 0.8.0 on pypi breaks build

There is a new version (0.8.0) on pypi.
This version is not mentioned in the change log on read the docs, nor is it published as a release here on github.

I use django-two-factor-auth==1.10.0 (with Django 2.2.10 and python 3.7.6), which used to install django-otp==0.7.5 As of yesterday, since there are no further constraints, it installed django-otp==0.8.0.

And since today/yesterday my builds break, because:

File "/home/service/.local/lib/python3.7/site-packages/django/db/models/fields/__init__.py", line 801, in get_default
     return self._get_default()
   File "/home/service/.local/lib/python3.7/site-packages/two_factor/models.py", line 57, in random_hex_str
     return random_hex().decode('utf-8')
 AttributeError: 'str' object has no attribute 'decode'

After forcing django-otp==0.7.5 it works again.

Feature request: Hash StaticToken.token

Related to #67, a similar issue is the StaticToken.token field. We think it would be beneficial (if not imperative) for the field to only include a salted hash of the token.

This feature would make life much more difficult for an attacker who got access to the DB or a dump thereof.

We will gladly contribute code if you are open to it.

DeviceManager.devices_for_user generates invalid sql where clause

The line

devices = devices.filter(confirmed=bool(confirmed))
(models.py line 32)

generates a sql statement with the following where clause

WHERE ([otp_totp_totpdevice].[user_id] = 1 AND [otp_totp_totpdevice].[confirmed])

changing this where clause to

WHERE ([otp_totp_totpdevice].[user_id] = 1 AND [otp_totp_totpdevice].[confirmed] > 0)

fixes the error.

This is with

Django==3.1
django-otp==1.0.0
djangorestframework==3.11.1
django-mssql-backend==2.8.1
pyodbc==4.0.30

running against:
MS SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0
ODBC Driver 17 for SQL Server

How to set the site name in the QR instead of "example.com"

Hi!

I'm looking for a way to set the name of the site/product into something different than "example.com". I tried changing the "site name" and url in django sites and restarted django to no avail.

I'm using an iphone using the google authenticator app. The authetnicator app says that i've scanned a QR on example.com.

I found the OTP_HOTP_ISSUER is not of relevance. I'd expect the site to be used, or that it at least configurable somewhere.

How to configure this?

Regards,
Elger

Throttling on token generation/delivery missing

The new SideChannelDevice added a generic way to generate, expire and verify tokens that are sent to the user via another channel. There is also a ThrottlingMixin for throttling the verify calls.
Sometimes, like with SMS, every token costs a bit of money. It would be good to add some generic way to throttle the sending of those tokens in order to limit the costs for the website owner in case of an attack.
I think a good place would be the ThrottlingMixin, but I'm not sure what would be the best way to implement it.

Feature request: Encrypt TOTPDevice.key

Hello,

First of all thank you very much for this project. It does a lot of heavy lifting and is well-documented.

During our security review, one of our developers pointed out that the TOTPDevice.key is stored plainly in the DB. Were it to be exposed, an attacker could use it without any difficulty.

We would like to protect against that and contribute back to the project by find a way to making the field optionally encrypt-able without introducing new dependencies. Is this something you would be open to discuss and possibly wish to have a PR for?

Custon Divice

It is possible to have a custom device, so you can add a couple of fields to the standard device.

Thanks

Conflicting user.is_verified attribute

Hi,

Our user model already has a database field called is_verified, which conflicts with the user.is_verified() method that django-otp adds to the user object in the OTPMiddleware._verify_user method.

To solve this we simply subclass and monkey-patch OTPMiddleware, but it would be nicer to have this work out of the box. Would it be possible to move is_verified to another name? For example, everything could be namespaced under user.otp:

user.otp.device
user.otp.is_verified()

I'd be happy to provide a PR if this seems like the right approach.

No option to disable logging of static tokens

When using the 'addstatictoken' management command, the text of the token is logged using self.stdout.write. Is there a way to optionally disable this logging if I don't want it?

Existing Admin Users Cannot Enable 2FA

If you already have existing admin users and then install the django-otp package, those already existing admin users cannot enable 2FA.

There should be some way to detect this or also have the existing admins be able to enable 2FA.

Otherwise the only workaround is to migrate the database first, set up the existing admins, then enable admin.site.class = OTPAdminSite to urls.py. Perhaps you could close this issue just add this to the docs to make it clear for users to migrate to the database first then setup the existing admins then enable that 1 line of code.

If using docker, be sure to comment out admin.site.class = OTPAdminSite in urls.py and any related OTP custom forms.py code if you have any. Then run docker container restart <container_id> to be able to login as an admin user to then setup OTP tokens for that admin user.

Not receiving same OTP in a time step/interval .

I have set time step as 300sec and TOTP digit as 6. I'm not receiving same OTP in a time step/interval, means receiving different OTP in a given time step. ie, At 2019-11-03 19:15:00, i will get one code; 19:13:00 , i will get another new code.

this is the code:

import datetime
import time

from django_otp.oath import TOTP
from django_otp.util import random_hex


class TOTPVerification:

    def __init__(self):
        # secret key that will be used to generate a token,
        # User can provide a custom value to the key.
        self.key = random_hex(20)
        # counter with which last token was verified.
        # Next token must be generated at a higher counter value.
        self.last_verified_counter = -1
        # this value will return True, if a token has been successfully
        # verified.
        self.verified = False
        # number of digits in a token. Default is 6
        self.number_of_digits = 6
        # validity period of a token. Default is 30 second.
        self.token_validity_period = 300

    def totp_obj(self):
        # create a TOTP object
        totp = TOTP(key=self.key,
                    step=self.token_validity_period,
                    digits=self.number_of_digits)
        # the current time will be used to generate a counter
        totp.time = time.time()
        return totp

    def generate_token(self):
        # get the TOTP object and use that to create token
        totp = self.totp_obj()
        # token can be obtained with `totp.token()`
        token = str(totp.token()).zfill(6)
        return token

    def verify_token(self, token, tolerance=0):
        try:
            # convert the input token to integer
            token = int(token)
        except ValueError:
            # return False, if token could not be converted to an integer
            self.verified = False
        else:
            totp = self.totp_obj()
            # check if the current counter value is higher than the value of
            # last verified counter and check if entered token is correct by
            # calling totp.verify_token()
            if ((totp.t() > self.last_verified_counter) and
                    (totp.verify(token, tolerance=tolerance))):
                # if the condition is true, set the last verified counter value
                # to current counter value, and return True
                self.last_verified_counter = totp.t()
                self.verified = True
            else:
                # if the token entered was invalid or if the counter value
                # was less than last verified counter, then return False
                self.verified = False
        return self.verified


if __name__ == '__main__':
    # verify token the normal way
    phone1 = TOTPVerification()
    start_time = time.time()
    expiry_time = time.time() + phone1.token_validity_period
    i = datetime.timedelta(seconds=start_time)
    j = datetime.timedelta(seconds=expiry_time)
    print('Start time:', start_time, str(i))
    print('Expiry time:', expiry_time, str(j))
    while time.time() < expiry_time:
        k = datetime.timedelta(seconds=time.time())
        generated_token = phone1.generate_token()
        print("Generated token is: {} , current time is :{}".format(generated_token, k))
        time.sleep(35)

Result:

('Start time:', 1573004969.106607, '18206 days, 1:49:29.106607')
('Expiry time:', 1573005269.106608, '18206 days, 1:54:29.106608')
Generated token is: 400250 , current time is :18206 days, 1:49:29.107410
Generated token is: 219130 , current time is :18206 days, 1:50:04.109554
Generated token is: 219130 , current time is :18206 days, 1:50:39.115040
Generated token is: 219130 , current time is :18206 days, 1:51:14.118708
Generated token is: 219130 , current time is :18206 days, 1:51:49.124145
Generated token is: 219130 , current time is :18206 days, 1:52:24.124820
Generated token is: 219130 , current time is :18206 days, 1:52:59.126073
Generated token is: 219130 , current time is :18206 days, 1:53:34.126536
Generated token is: 219130 , current time is :18206 days, 1:54:09.131970

Where i'm doing wrong , did i miss some thing..?

Provide time at which throttling lock expires

When a Device is "locked" due to throttling, ThrottlingMixin.verify_is_allowed() returns (False, info_dict). info_dict contains the number of failed attempts but provides no information about how long or until when the device is locked due to throttling. I assume that in may cases this information would be helpful for the user and I think it could easily be provided by verify_is_allowed().

I suggest that a new key is added to info_dict, e.g. {..., 'locked_until': datetime.datetime(…)} or something similar. What do you think?

ISE during verification in LoginView

Hi,

Many thanks for this wonderful project!

I found the following issue with LoginView. When verifying a user that is already authenticated, the LoginView uses OTPTokenForm. Unlike OTPAuthentication, OTPTokenForm does not call authenticate() when validating the form data. The authenticate() function populates the user with a backend attribute. Therefore, during verification of authenticated users, the user object is left without this attribute. If the token is valid, the user is logged in by calling login(). This function expects the backend attribute. If it does not exist, it uses the backend, configured in settings. But if more than one backend is configured, it throws an error. Therefore, verifying authenticated users, with multiple configured auth backends, is not supported out of the box. Having more than one backend is not uncommon or exotic. In my case, I have two backends - one from django-axes and one from django-allauth.

django-otp version: 0.7.1

The following stacktrace is produced:

Internal Server Error: /backend/login/
Traceback (most recent call last):
........
  File "/Users/victor/anaconda3/envs/myapp/lib/python3.7/site-packages/django/contrib/auth/views.py", line 90, in form_valid
    auth_login(self.request, form.get_user())
  File "/Users/victor/anaconda3/envs/myapp/lib/python3.7/site-packages/django/contrib/auth/__init__.py", line 118, in login
    'You have multiple authentication backends configured and '
ValueError: You have multiple authentication backends configured and therefore must provide the `backend` argument or set the `backend` attribute on the user.

I've worked around this problem with the following subclass:

from django_otp.views import LoginView
from django.contrib.auth import BACKEND_SESSION_KEY

class CustomLoginView(LoginView):
    def form_valid(self, form):
        user = form.get_user()
        if not hasattr(user, 'backend'):
            user.backend = self.request.session[BACKEND_SESSION_KEY]
        return super().form_valid(form)

I hope this helps. Please let me know if you need more information, or if you'd like a pull request.

Best regards,
Victor

Cannot see the admin login form field

@claudep @psagers

Tried following this video tutorial exactly and I am unable to see the OTP form fields on the admin login page. It just shows the username and password fields only.

https://www.youtube.com/watch?v=TGvK5z9R8Ik

Code from the video is in the comments. Try sorting by new in youtube if the code comment does not show up at first.

As an admin user, I can login without a token even though in the admin backend OTP devices are enabled for that user.

When overriding the login.html template in my project/templates/admin/login.html and pasting in this entire code:
https://github.com/django-otp/django-otp/blob/master/src/django_otp/templates/otp/admin111/login.html

It only shows the label text "OTP Token" and the input tag itself is missing as if {{ form.otp_token }} and {{ form.otp_device }} does not work.

Deleting the {% if form.get_user %} if statement shows the "OTP Device" label but not the input tag.

Seems like a lot more steps to override the login.html template compared to the steps the video. Did something change for the worst?

Might be related to #26 which was closed.

Fix Django 4.0 deprecation warning

Hey, upgraded django to 3.1 and started getting the following warnings:

django_otp/plugins/otp_totp/admin.py:90: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path().
    url(r'^(?P<pk>\d+)/qrcode/$', self.admin_site.admin_view(self.qrcode_view), name='otp_totp_totpdevice_qrcode'),

AttributeError: 'str' object has no attribute 'decode' after updating to v0.8.0

Hi!

I just tried the new version 0.8.0 but get this error when running my tests (during test database creation):

Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/commands/test.py", line 23, in run_from_argv
    super().run_from_argv(argv)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/commands/test.py", line 53, in handle
    failures = test_runner.run_tests(test_labels)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/test/runner.py", line 629, in run_tests
    old_config = self.setup_databases(aliases=databases)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/test/runner.py", line 554, in setup_databases
    self.parallel, **kwargs
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/test/utils.py", line 174, in setup_databases
    serialize=connection.settings_dict.get('TEST', {}).get('SERIALIZE', True),
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/base/creation.py", line 72, in create_test_db
    run_syncdb=True,
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/__init__.py", line 148, in call_command
    return command.execute(*args, **defaults)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/base.py", line 83, in wrapped
    res = handle_func(*args, **kwargs)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 234, in handle
    fake_initial=fake_initial,
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/migrations/executor.py", line 117, in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/migrations/executor.py", line 147, in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/migrations/executor.py", line 245, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/migrations/operations/fields.py", line 249, in database_forwards
    schema_editor.alter_field(from_model, from_field, to_field)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 535, in alter_field
    old_db_params, new_db_params, strict)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/postgresql/schema.py", line 124, in _alter_field
    new_db_params, strict,
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 649, in _alter_field
    new_default = self.effective_default(new_field)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 233, in effective_default
    return field.get_db_prep_save(self._effective_default(field), self.connection)
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/backends/base/schema.py", line 212, in _effective_default
    default = field.get_default()
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/django/db/models/fields/__init__.py", line 801, in get_default
    return self._get_default()
  File "/home/ellen/.virtualenvs/sfe/lib/python3.6/site-packages/two_factor/models.py", line 57, in random_hex_str
    return random_hex().decode('utf-8')
AttributeError: 'str' object has no attribute 'decode'

It only happens when I do this update and reverting back to 0.7.5 runs my tests normally.
Do you have any idea what the issue is? I'm running Python 3.6 and Django 2.2.10 btw.

Django-allauth Staff Able To Login To Admin Without OTP Token From The Frontend

Using django-allauth, a staff admin user is able to login normally from the frontend of my site. Then when that staff user tries to access the /admin/ login page, they are able to bypass the login form which asks for the OTP token, and are logged into admin automatically.

This is also the case without this package installed. But it would be nice to have an option like a setting to include a way to require admin staff users who are logged into the frontend to be forced to enter the OTP token on the /admin/ login backend page every time they try to access the /admin/ URL.

An idea would be to make it so staff must use the OTP token to log in no matter which login view they use. Or you could block access to the frontend for staff users.

Issue with migrations

I have installed django otp for my project and I have migrated all the changes to my database and when I tried to create migration files for my apps using the below command

**python manage.py makemigrations**

django-otp creates some migration files which deletes EmailDevice, StaticDevice, StaticToken, TotpDevice tables of django-otp. Please help me to resolve this issue.
I am attaching those 3 migration files which is created in effect of this command.

Screenshot 2021-03-18 at 2 57 29 PM

Screenshot 2021-03-18 at 2 58 08 PM
Screenshot 2021-03-18 at 2 58 33 PM

I am using python 3.9 and django 3.1.7
Please help me to resolve this issue.

Having Trouble Validating Recaptcha v3

I want to add recaptcha v3 to this package's admin login page which I was able to do (shown in stackoverflow link) but cannot figure out how to validate it (I can't print anything and therefore cannot test it). Using django-allauth also if that matters.

https://stackoverflow.com/questions/62766688/django-2fa-otp-and-recaptcha-v3-on-the-admin-login-page

Found a similar question for customizing views with no answer https://stackoverflow.com/questions/54534809/django-otp-implementation-using-custom-views

For some reason placing the recaptcha validation code in class LoginView(auth_views.LoginView) does not want to be printed.

Recaptcha views.py validation template:

    form = Form()

    if request.method == 'POST':
        form = Form(data=request.POST)

        if form.is_valid():


            recaptcha_response = request.POST.get('recaptcha_token')
            url = 'https://www.google.com/recaptcha/api/siteverify'
            payload = {'secret': 'SECRET','response': recaptcha_response}
            data = urllib.parse.urlencode(payload).encode()
            req = urllib.request.Request(url, data=data)

            response = urllib.request.urlopen(req)
            result = json.loads(response.read().decode())

            print('result:' + str(result))

            print('recaptcha_response:' + str(recaptcha_response))

            print('result_success:' + str(result['success']))

            if (not result['success']) or (not result['action'] == '/admin/login/'):
                messages.error(request, 'Invalid reCAPTCHA. Please try again.')

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.