django-otp is a low level framework for one-time passwords in Django
A pluggable framework for adding two-factor authentication to Django using one-time passwords.
At a glance
- What is it?
- django-otp lets a Django project add one-time password authentication, shipping HOTP and TOTP implementations and a plugin interface for other devices, and it integrates with django.contrib.auth without being an authentication backend. The author states plainly that the project is stable and maintained but no longer actively used by him, and the licence is the Unlicense.
- Who is it for?
- django-otp suits a Django team that wants to compose its own two-factor flow, plugging in a specific device or verification method and controlling where in the login sequence it applies, and it is the wrong choice for a project that wants a complete, opinionated second factor, since the README itself points to django-two-factor-auth for that.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A framework, not a second factor you can switch on
django-otp makes it easy to add support for one-time passwords to Django, and it can be integrated at various levels depending on how much customization is required.
That last clause is the whole design in one sentence. This is not a login view with a toggle for two-factor authentication. It is a set of primitives that you wire into your own views, and the integration level you choose decides how much of the flow you own.
It integrates with django.contrib.auth, and the documentation is careful to add that although it does so, it is not a Django authentication backend. That distinction has consequences. A backend plugs into Django's auth machinery and produces a user object; django-otp instead gives you devices attached to a user, and a verification state that your views consult. If you want a second factor as an opaque part of login(), this is not the shape of it. If you want to decide when a user must re-verify, which is where the project is most interesting, it is exactly the shape of it.
The primary target is stated as 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, and the package includes an implementation of OATH HOTP and OATH TOTP for convenience, since those are the standard algorithms used by multiple plugins.
The package metadata gives the version as 1.7.5, requires Python 3.8 or newer and Django 4.2 or newer, and carries the Development Status classifier for production and stable.
The licence is the Unlicense and the status is stated plainly
Two things in the project documentation deserve to be read before anything else, and neither of them is in a changelog.
The first is the Status section, and it is unusually candid. The 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, with the pointer to CONTRIBUTING.rst, and general questions and ideas should go to Discussions while issues are reserved for confirmed bugs.
That is a maintenance statement, and it should shape a dependency decision. Stable and maintained is not the same as actively developed, and a framework that is no longer used by its author will accumulate compatibility work for new Django releases that someone else has to do. The repository was last pushed to on 2026-10-16, and it publishes no GitHub releases, so the version in the package metadata is the version you install.
The second is the licence, which is the Unlicense. That is a public domain dedication rather than a conventional permissive licence, and the package metadata records it as such, with a matching classifier. The practical effect is that the code carries no conditions: no attribution requirement, no copyleft, no patent grant language beyond what the text says. For a company embedding a security component, that removes a category of legal review.
The two facts sit oddly together, a very permissive licence on a project with no ongoing investment, and that combination is a reasonable summary of where this dependency sits in 2026.
Hatch environments and a justfile for the developer workflow
The development setup of django-otp is modern and well documented, and it is the part of the repository a contributor will actually use.
The project is built and managed with hatch, and the README recommends installing it with pipx if you do not have it. A justfile is provided with wrappers for the scripts in pyproject.toml, and the documentation says the easiest way to get oriented is to run just, which lists the available commands.
The pyproject.toml defines several useful scripts. The default environment includes all dev and test dependencies for quickly running tests, and 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, which is also what makes the runserver target possible: just runserver starts a simple interactive test environment, and the project under test has an implementation of the login form and views with different combinations of decorators, which you can experiment with or use to test changes.
The lint group is three tools in one command, flake8, isort in check mode and black in check mode, with a fix target that repairs what they report. There are separate targets for tests, tests with all warnings enabled, tests with coverage, and the full matrix with just test-all, which the documentation notes you will need multiple specific Python versions installed for.
Hatch environments are cleaned with hatch env prune, which the README gives as an example of how to force dependency updates. The release flow in the justfile runs bumpversion, then hatch clean, hatch build and hatch publish.
Test configuration lives in TOML files, and SQLite skips the concurrency tests
The configuration story is unusual for a Django library, and it exists to serve the test matrix rather than your production settings.
Configuration is taken from TOML files stored under test/config. A config file named env-<env-name>.toml is automatically applied when running inside a matching hatch environment, so env-default.toml applies to the default development environment and env-test.toml applies to the test matrix environments. The documented example for a wide-open PostgreSQL install looks like this:
[database]
ENGINE = "django.db.backends.postgresql"
NAME = "django-otp"
USER = "postgres"For development, the same file can inject Django apps and middleware, or override arbitrary Django settings, and test/config/sample.toml is the full description of what is possible. You can also force a specific config file by setting the environment variable DJANGO_OTP_CONFIG to a path, which is the escape hatch when the automatic env-name matching is not what you want.
The database detail matters for anyone running the suite. By default the test project uses SQLite, and because SQLite does not support row locking, some concurrency tests will be skipped. That is not a cosmetic gap. Row locking is what a one-time password device needs to avoid handing the same token to two concurrent verification attempts, so the tests that would catch that race are exactly the ones that do not run on the default configuration.
For a project evaluating this library, that is the detail to reproduce first: point the test environment at PostgreSQL and run the suite there.
The author describes the design he would build next
The README ends with a section called The Future, and it is the most revealing part of the documentation because it is the author arguing with his own design.
The argument runs like this. Once upon a time everything was usernames and passwords, and 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 is increasingly common to see sites with more nuanced authentication state. A site might remember who you are forever, so you are not anonymous, but if you try to do anything private you have to re-authenticate. You may be able to choose from among all 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.
The conclusion he draws is that the world seems to be moving beyond the assumptions that originally informed Django's essential authentication design. If he were still investing in Django generally, he would probably start a new multi-factor authentication project reflecting those changes, incorporating 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.
That last point is the design gap, stated by the person who wrote the code. The current model couples how long you are authenticated with how strongly, and a framework that wanted to model the newer world would separate them. He closes by offering advice to anyone who wants to take on such a project.
Where django-otp stops and an opinionated package begins
The README itself points at the alternative, and it is a useful one because the difference is in approach rather than in feature count.
If you are looking for a higher-level or more opinionated solution, the documentation suggests django-two-factor-auth. That distinction is the whole comparison. django-otp gives you devices, a verification state and plugin interfaces, and you decide the flow, the integration level and where re-authentication is required. An opinionated package makes those decisions for you and gives you a working two-factor login out of the box.
The choice follows from what your project needs. If your authentication requirements are the usual ones, a second factor at login, then the opinionated route is less work and more predictable, and you inherit its decisions rather than making them. If your requirements are the newer ones the author describes, where a user is remembered but a private action requires a fresh factor, or where a particular mechanism is required for a particular action, then the pluggable design is the only one of the two that can express it, and the cost is that you build the flow yourself.
There is a third path worth naming, which is writing the verification yourself against django.contrib.auth. It is more work and it is what django-otp already packages, so the question is only whether the plugin interface and the extra OTP algorithms are worth taking as a dependency given the maintenance status described above.
Editorial conclusion
django-otp suits a Django team that wants to compose its own two-factor flow, plugging in a specific device or verification method and controlling where in the login sequence it applies, and it is the wrong choice for a project that wants a complete, opinionated second factor, since the README itself points to django-two-factor-auth for that. Before adopting it, read the Status section, because the author says the project is no longer actively used by him and is not seeing much ongoing investment, and plan for that when you pick a dependency.
Frequently asked questions
What is django-otp?
It is a pluggable framework for adding one-time password support to Django, usable at various integration levels depending on how much customization is required. It includes implementations of the OATH HOTP and TOTP algorithms and ships several OTP plugins, with more available separately.
Is django-otp an authentication backend?
No. It integrates with django.contrib.auth, but the documentation is explicit that it is not a Django authentication backend. It provides devices and a verification state that your own views consult, which is what allows a project to decide when re-authentication is required.
What is the maintenance status of django-otp?
The project documentation states that it 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, general questions belong in Discussions, and issues are reserved for confirmed bugs.
How do I run the django-otp test suite?
With hatch and just. Run just to list the commands, just test for the default environment, just test-all for the full matrix, which needs several specific Python versions, and just runserver for an interactive test project with a login form. Hatch environments can be cleaned with hatch env prune.
Why are some django-otp concurrency tests skipped?
Because the test project uses SQLite by default, and SQLite does not support row locking. To run those tests, point the environment at PostgreSQL with a configuration file under test/config, where a file named env-<env-name>.toml is applied automatically inside the matching hatch environment.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/django-otp-django-otp)Was this useful?