A => BSD-LICENSE +32 -0
@@ 0,0 1,32 @@
+Copyright (c) 2020, Netlandish Inc. <https://www.netlandish.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+ * Neither the name of Netlandish Inc. nor the names of project
+ contributors may be used to endorse or promote products
+ derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
M MANIFEST.in +2 -0
@@ 1,2 1,4 @@
+include BSD-LICENSE
+include README.rst
include README.md
recursive-include nlotp/templates *
M README.md +65 -12
@@ 1,4 1,4 @@
- nlotp [![nlshield](https://img.shields.io/badge/100%25-Netlandish-blue.svg?style=square-flat)](http://www.netlandish.com)
+django-nlotp [![nlshield](https://img.shields.io/badge/100%25-Netlandish-blue.svg?style=square-flat)](http://www.netlandish.com)
----
Django application to quickly provide you with TOTP with recovery codes right
@@ 14,20 14,73 @@ out of the box.
[issues]: https://todo.code.netlandish.com/~netlandish/django-nlotp
[pinbox]: https://lists.code.netlandish.com/~netlandish/public-inbox
-# Netlandish One-Time Password (Two-Factor Authentication)
+## Netlandish One-Time Password (Two-Factor Authentication)
+
+The Django app `django-nlotp` (Netlandish OTP) that provides ready to use views
+to setup [TOTP][] support as well as recovery codes in your project. It's
+basically views and a middleware that operates with the `django-otp`
+([GitHub][django-otp]) app, which does the heavy lifting.
+
+We created this because we use `django-otp` in several products as well as
+client projects. After using it twice in a very similar fashion we knew we
+needed to write an app that we can plug in to virtually any product and
+immediately have access to [OTP][] support.
-Netlandish OTP is a customization of
-[`django-otp`](https://github.com/django-otp/django-otp) app, using TOTP and
-Static plugins.
+This app should give you everything you need to quickly implement OTP in your
+project. You can use settings for both `django-nlotp` and `django-otp` apps to
+customize as you wish. This app should give you coverage for the majority of
+use cases you have. If you want to get deeper into the weeds then we'd
+recommend rolling your own custom approach direct with `django-otp`.
+
+[otp]: http://en.wikipedia.org/wiki/One-time_password
+[totp]: https://tools.ietf.org/html/rfc6238
+[django-otp]: https://github.com/django-otp/django-otp
+
+## What is Two-Factor Verification?
+
+Two Factor Verification (2FA) makes your account more secure by requiring two
+things in order to log in: *something you know* and *something you own*.
+
+## How does it work?
+
+"Something you know" is your username and password, while "something you own"
+can be an application to generate a temporary code.
-The user verification process is done by first checking a TOTP device match, if
-not found look for user matching Static device (backup codes).
+Users who have chosen to set up 2FA will be asked to provide their second
+method of identity verification during the log in process.
+
+## How does 2FA with an authentication application (TOTP) work?
+
+Users can set up 2FA using any authentication application that supports
+the TOTP standard.
+
+TOTP authentication applications generate a regularly changing authentication
+code to use when logging into your account.
+
+Because TOTP is an open standard, there are many applications that are
+compatible with your AnyHow account. Popular applications include:
+
+- Google Authenticator for [Android][gadroid] or [iOS][gaios]
+- [Microsoft Authenticator][msauth]
+- [Authy][authy]
-To enable one-time password, a TOTP verification code needs to be provided, by
-scanning the QR code or entering a key on an application like Google
-Authenticator. Once enabled, backup codes (Static device) are generated and
-Django user account password will be required for two actions: generating new
-backup codes and disabling 2FA.
+There are many others as well.
+
+[gadroid]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2
+[gaios]: https://itunes.apple.com/app/google-authenticator/id388497605
+[msauth]: https://www.microsoft.com/en-us/account/authenticator
+[authy]: https://authy.com/
+
+## User Verification
+
+By default in `django-nlotp` the user verification process (when the user is to
+enter their one time password) will check the password submitted in the
+following order:
+
+1. Check for a TOTP device match. In other words, check that the code matches
+ the configured user device.
+2. If it doesn't match, then it checks if the code is a user's backup code. If
+ so, it allows the user access and removes the provided code from future use.
## Installation
M nlotp/__init__.py +22 -1
@@ 1,3 1,24 @@
+# -*- coding: utf-8 -*-
default_app_config = "nlotp.apps.NLOTPConfig"
+VERSION = (0, 1, 0, "final", 0)
+
+
+def get_version():
+ "Returns a PEP 386-compliant version number from VERSION."
+ assert len(VERSION) == 5
+ assert VERSION[3] in ("alpha", "beta", "rc", "final")
-__version__ = "0.1.0"
+ # Now build the two parts of the version number:
+ # main = X.Y[.Z]
+ # sub = .devN - for pre-alpha releases
+ # | {a|b|c}N - for alpha, beta and rc releases
+
+ parts = 2 if VERSION[2] == 0 else 3
+ main = ".".join(str(x) for x in VERSION[:parts])
+
+ sub = ""
+ if VERSION[3] != "final":
+ mapping = {"alpha": "a", "beta": "b", "rc": "c"}
+ sub = mapping[VERSION[3]] + str(VERSION[4])
+
+ return str(main + sub)
A => pyproject.toml +29 -0
@@ 0,0 1,29 @@
+[tool.black]
+line-length = 79
+target-version = ['py37', 'py38']
+include = '\.pyi?$'
+exclude = '''
+
+(
+ /(
+ \.eggs # exclude a few common directories in the
+ | \.git # root of the project
+ | \.hg
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | env
+ | _build
+ | buck-out
+ | build
+ | dist
+ )/
+ | foo.py # also separately exclude a file named foo.py in
+ # the root of the project
+)
+'''
+
+[tool.isort]
+line_length=79
+multi_line_output=5
+include_trailing_comma=true
A => requirements.txt +3 -0
@@ 0,0 1,3 @@
+Django>=3.0
+django-otp>=0.9.3
+qrcode>=6.1
M setup.py +19 -28
@@ 1,43 1,35 @@
import os
-from setuptools import setup
+from setuptools import find_packages, setup
project_name = "nlotp"
-long_description = open("README.md").read()
-# Idea from django-registration setup.py
-packages, data_files = [], []
-root_dir = os.path.dirname(__file__)
-if root_dir:
- os.chdir(root_dir)
+if os.path.exists("README.rst"):
+ long_description = open("README.rst").read()
+else:
+ long_description = (
+ "See https://hg.code.netlandish.com/~netlandish/django-nlotp"
+ )
-for dirpath, dirnames, filenames in os.walk(project_name):
- # Ignore dirnames that start with '.'
- for i, dirname in enumerate(dirnames):
- if dirname.startswith("."):
- del dirnames[i]
- if "__init__.py" in filenames:
- pkg = dirpath.replace(os.path.sep, ".")
- if os.path.altsep:
- pkg = pkg.replace(os.path.altsep, ".")
- packages.append(pkg)
- elif filenames:
- prefix = dirpath[(len(project_name) + 1):]
- for f in filenames:
- data_files.append(os.path.join(prefix, f))
+def parse_requirements(filename):
+ """ load requirements from a pip requirements file """
+ lineiter = (line.strip() for line in open(filename))
+ return [line for line in lineiter if line and not line.startswith("#")]
+
+
+reqs = parse_requirements("requirements.txt")
setup(
name=project_name,
- version=__import__("nlotp").__version__,
+ version=__import__(project_name).get_version(),
package_dir={project_name: project_name},
- packages=packages,
- package_data={project_name: data_files},
+ packages=find_packages(),
description="Netlandish one-time passwords custom setup.",
author="Netlandish Inc.",
- author_email="gustavo@netlandish.com",
+ author_email="hello@netlandish.com",
license="BSD License",
- url="https://hg.code.netlandish.com/~netlandish/nlotp",
+ url="https://hg.code.netlandish.com/~netlandish/django-nlotp",
long_description=long_description,
platforms=["any"],
classifiers=[
@@ 50,10 42,9 @@ setup(
"Environment :: Web Environment",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
- install_requires=["Django>=3.0", "django-otp>=0.9.3", "qrcode>=6.1"],
+ install_requires=reqs,
)