A django app that allows supplementing the default django.contrib.auth
login
system with two-factor authentication using the Time-Based One-Time Password
(TOTP) Algorithm spec (RFC 6238).
Google Authenticator is currently the primary support target for tokens, but any TOTP (soft?) token that allows using a server-generated seed should work.
Some notes:
- RFC 6238 - TOTP: Time-Based One-Time Password Algorithm
- ...An extension of RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm
- A proof of concept regarding how to use python-oath with Google Authenticator
This app is a work in progress. It is not much farther than "proof of concept," however. Clone the source and look at twofactor_demo/README.mdown for a quick demo (including steps on how to set it up).
The basics work:
- If the special
adminsite
subclass is used, all login forms are replaced with one that has an optional "Auth Code" field. (Users with two-factor auth enabled will be required to enter this.) - In the admin, a "Two-factor Authentication" link shows up next to the "Change password" link. From here, a user may enable two-factor auth (if it is not enabled), reset their auth token (to allow migration to a new device), or disable two-factor auth (if it is enabled).
What does not work:
- (Optional) backup codes or some ability to reset your auth token (or even simply log in) if you have lost your token. Optional because in some usecases this is a feature for high security that require manual intervention (support call to have a human verify and reset). FWIW, Google does provide this in the form of temporary, one-time-use codes that are received when two-factor authentication is turned on.
- python-oath -- not to be confused with "OAuth"
- django
Optionally, for performance:
- PyCrypto -- See PyCrypto section below, under Security Considerations.
Section 5.1 of RFC 6238 "recommends" that the shared secret key (i.e. the "seed" for the time-based generator) be stored securely in the validation system (i.e., the server) and specifies encrypting the values until their use is necessary.
The key cannot be hashed (as with a password) because the raw value of the key
is required to seed the generator. A single round of AES against
settings.SECRET_KEY
is used (with a randomly-generated seed appended to
SECRET_KEY
) as the encryption passphrase so that the value can be decrypted
at validation time.
This works sort of like this:
stored_seed = salt + "$" + hexlify(AES(
key = sha256(SECRET_KEY + salt),
value = raw_seed
))
# i.e. 'CYM5yCSZ9Ybyu1dq$6cc094ca2e1eb46122d84ae877fff885'
This isn't perfect, but it obscures the values in the database in the event
the database is compromised but settings.SECRET_KEY
has not been.
A copy of pyaes is bundled with this app. If PyCrypto is installed, that library's C-compiled extensions will be used instead of the (extremely slow) native Python AES implementation.