aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--okupy/accounts/forms.py5
-rw-r--r--okupy/accounts/views.py42
-rw-r--r--okupy/common/crypto.py5
-rw-r--r--okupy/common/models.py7
-rw-r--r--okupy/otp/models.py10
-rw-r--r--okupy/otp/sotp/models.py17
-rw-r--r--okupy/otp/totp/models.py34
7 files changed, 99 insertions, 21 deletions
diff --git a/okupy/accounts/forms.py b/okupy/accounts/forms.py
index 29c0248..a82bba9 100644
--- a/okupy/accounts/forms.py
+++ b/okupy/accounts/forms.py
@@ -11,6 +11,11 @@ class LoginForm(forms.Form):
label='Password:')
+class StrongAuthForm(forms.Form):
+ password = forms.CharField(max_length=30, widget=forms.PasswordInput(),
+ label='Password:')
+
+
class OpenIDLoginForm(LoginForm):
auto_logout = forms.BooleanField(required=False,
label='Log out after answering the OpenID request')
diff --git a/okupy/accounts/views.py b/okupy/accounts/views.py
index dcfbadf..e245d43 100644
--- a/okupy/accounts/views.py
+++ b/okupy/accounts/views.py
@@ -29,7 +29,7 @@ from passlib.hash import ldap_md5_crypt
from urlparse import urljoin, urlparse, parse_qsl
from .forms import (LoginForm, OpenIDLoginForm, SSLCertLoginForm,
- OTPForm, SignupForm, SiteAuthForm)
+ OTPForm, SignupForm, SiteAuthForm, StrongAuthForm)
from .models import LDAPUser, OpenID_Attributes, Queue
from .openid_store import DjangoDBOpenIDStore
from ..common.ldap_helpers import (get_bound_ldapuser,
@@ -40,6 +40,7 @@ from ..common.decorators import strong_auth_required
from ..common.exceptions import OkupyError
from ..common.log import log_extra_data
from ..otp import init_otp
+from ..otp.models import RevokedToken
from ..otp.sotp.models import SOTPDevice
from ..otp.totp.models import TOTPDevice
@@ -84,9 +85,16 @@ def login(request):
next = request.REQUEST.get('next') or reverse(index)
is_otp = False
login_form = None
- login_form_class = OpenIDLoginForm if oreq else LoginForm
strong_auth_req = 'strong_auth_requested' in request.session
+ if oreq:
+ login_form_class = OpenIDLoginForm
+ elif ('strong_auth_requested' in request.session
+ and request.user.is_authenticated()):
+ login_form_class = StrongAuthForm
+ else:
+ login_form_class = LoginForm
+
try:
if request.method != 'POST':
pass
@@ -108,6 +116,9 @@ def login(request):
else:
raise OkupyError('OTP verification failed')
+ # prevent replay attacks and race conditions
+ if not RevokedToken.add(request.user, token):
+ raise OkupyError('OTP verification failed')
dev = django_otp.match_token(request.user, token)
if not dev:
raise OkupyError('OTP verification failed')
@@ -115,7 +126,10 @@ def login(request):
else:
login_form = login_form_class(request.POST)
if login_form.is_valid():
- username = login_form.cleaned_data['username']
+ if login_form_class != StrongAuthForm:
+ username = login_form.cleaned_data['username']
+ else:
+ username = request.user.username
password = login_form.cleaned_data['password']
else:
raise OkupyError('Login failed')
@@ -149,13 +163,17 @@ def login(request):
logger.critical(error, extra=log_extra_data(request))
logger_mail.exception(error)
raise OkupyError("Can't contact LDAP server")
- if (request.user.is_authenticated()
- and (not strong_auth_req
- or 'secondary_password' in request.session)):
- if request.user.is_verified():
- return redirect(next)
- login_form = OTPForm()
- is_otp = True
+ if request.user.is_authenticated():
+ if (strong_auth_req
+ and not 'secondary_password' in request.session):
+ if request.method != 'POST':
+ messages.info(request, 'You need to type in your password'
+ + ' again to perform this action')
+ else:
+ if request.user.is_verified():
+ return redirect(next)
+ login_form = OTPForm()
+ is_otp = True
if login_form is None:
login_form = login_form_class()
@@ -370,6 +388,10 @@ def otp_setup(request):
if not conf_form.is_valid():
raise OkupyError()
token = conf_form.cleaned_data['otp_token']
+
+ # prevent reusing the same token to login
+ if not RevokedToken.add(request.user, token):
+ raise OkupyError()
if not dev.verify_token(token, secret):
raise OkupyError()
except OkupyError:
diff --git a/okupy/common/crypto.py b/okupy/common/crypto.py
index 57694c0..2596680 100644
--- a/okupy/common/crypto.py
+++ b/okupy/common/crypto.py
@@ -51,6 +51,11 @@ cipher = OkupyCipher()
class IDCipher(object):
+ """
+ A cipher to create 'encrypted database IDs'. It is specifically fit
+ to encrypt an integer into constant-length hexstring.
+ """
+
def encrypt(self, id):
byte_id = struct.pack('!I', id)
byte_eid = cipher.encrypt(byte_id)
diff --git a/okupy/common/models.py b/okupy/common/models.py
index d1ef128..4a7ee73 100644
--- a/okupy/common/models.py
+++ b/okupy/common/models.py
@@ -17,10 +17,17 @@ class EncryptedPKModelManager(models.Manager):
class EncryptedPKModel(models.Model):
+ """
+ A model with built-in identifier encryption (for secure tokens).
+ """
+
objects = EncryptedPKModelManager()
@property
def encrypted_id(self):
+ """
+ The object identifier encrypted using IDCipher, as a hex-string.
+ """
if self.id is None:
return None
return idcipher.encrypt(self.id)
diff --git a/okupy/otp/models.py b/okupy/otp/models.py
index f3595cd..a43fb4e 100644
--- a/okupy/otp/models.py
+++ b/okupy/otp/models.py
@@ -15,6 +15,10 @@ class RevokedToken(models.Model):
@classmethod
def cleanup(cls):
+ """
+ Remove tokens old enough to be no longer valid.
+ """
+
# we use this just to enforce atomicity and prevent replay
# for SOTP, we can clean up old tokens quite fast
# (as soon as .delete() is effective)
@@ -24,6 +28,12 @@ class RevokedToken(models.Model):
@classmethod
def add(cls, user, token):
+ """
+ Use and revoke the given token, for the given user.
+
+ Returns True if the token is fine, False if it was used
+ already.
+ """
cls.cleanup()
t = cls(user=user, token=token)
diff --git a/okupy/otp/sotp/models.py b/okupy/otp/sotp/models.py
index 82949d2..5bb58f8 100644
--- a/okupy/otp/sotp/models.py
+++ b/okupy/otp/sotp/models.py
@@ -3,13 +3,21 @@
from django_otp.models import Device
from ...accounts.models import LDAPUser
-from ..models import RevokedToken
import random
class SOTPDevice(Device):
+ """
+ OTP device that verifies against a list of recovery keys in LDAP.
+ """
+
def gen_keys(self, user, num=10):
+ """
+ Generate new recovery keys for user and store them in LDAP.
+
+ Previous keys (if any) will be removed.
+ """
new_keys = set()
# generate the new keys the fun way
@@ -23,10 +31,9 @@ class SOTPDevice(Device):
return new_keys
def verify_token(self, token):
- # ensure atomic revocation
- if not RevokedToken.add(self.user, token):
- return False
-
+ """
+ Verify token against recovery keys.
+ """
u = LDAPUser.objects.get(username = self.user.username)
if token in u.otp_recovery_keys:
u.otp_recovery_keys.remove(token)
diff --git a/okupy/otp/totp/models.py b/okupy/otp/totp/models.py
index d61d17d..a32baa6 100644
--- a/okupy/otp/totp/models.py
+++ b/okupy/otp/totp/models.py
@@ -6,33 +6,59 @@ from django_otp.models import Device
from base64 import b32decode, b32encode
from ...accounts.models import LDAPUser
-from ..models import RevokedToken
import Crypto.Random
class TOTPDevice(Device):
+ """
+ OTP device that verifies against a TOTP-generated token.
+ """
+
def is_enabled(self):
+ """
+ Check whether TOTP is enabled.
+
+ Returns True if user has TOTP secret set, False otherwise.
+ """
return not self.verify_token()
def disable(self, user):
+ """
+ Disable TOTP. Removes the secret from LDAP.
+ """
if user.otp_secret:
user.otp_secret = None
user.save()
def enable(self, user, new_secret):
+ """
+ Enable TOTP. Saves the secret to LDAP.
+ """
user.otp_secret = new_secret
user.save()
def gen_secret(self):
+ """
+ Generate a new TOTP secret compliant with Google Authenticator.
+
+ Returns 20-character base32 string (with padding stripped).
+ """
rng = Crypto.Random.new()
return b32encode(rng.read(12)).rstrip('=')
@staticmethod
def get_uri(secret):
- return 'otpauth://totp/identity.gentoo.org?secret=%s' % secret
+ """
+ Get otpauth:// URI for secret transfer.
+ """
+ return 'otpauth://totp/gentoo.org?secret=%s' % secret
def verify_token(self, token=None, secret=None):
+ """
+ Verify the entered token against current TOTP token, and the few
+ past and future tokens to include clock drift.
+ """
if not secret:
u = LDAPUser.objects.get(username = self.user.username)
if not u.otp_secret:
@@ -41,10 +67,6 @@ class TOTPDevice(Device):
return False
secret = u.otp_secret
- # prevent replay attacks
- if not RevokedToken.add(self.user, token):
- return False
-
# add missing padding if necessary
secret += '=' * (-len(secret) % 8)