diff --git a/hc/accounts/backends.py b/hc/accounts/backends.py new file mode 100644 index 00000000..1d07c458 --- /dev/null +++ b/hc/accounts/backends.py @@ -0,0 +1,24 @@ +from django.contrib.auth.hashers import check_password +from django.contrib.auth.models import User +from hc.accounts.models import Profile + + +# Authenticate against the token in user's profile. +class ProfileBackend(object): + + def authenticate(self, username=None, token=None): + try: + profile = Profile.objects.get(user__username=username) + except Profile.DoesNotExist: + return None + + if not check_password(token, profile.token): + return None + + return profile.user + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None diff --git a/hc/accounts/migrations/0003_profile_token.py b/hc/accounts/migrations/0003_profile_token.py new file mode 100644 index 00000000..02f19ca9 --- /dev/null +++ b/hc/accounts/migrations/0003_profile_token.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-01-04 20:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_profile_ping_log_limit'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='token', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 65cdbcef..cf8274d1 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -1,5 +1,6 @@ from datetime import timedelta from django.conf import settings +from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User from django.core import signing from django.core.urlresolvers import reverse @@ -26,9 +27,19 @@ class Profile(models.Model): next_report_date = models.DateTimeField(null=True, blank=True) reports_allowed = models.BooleanField(default=True) ping_log_limit = models.IntegerField(default=100) + token = models.CharField(max_length=128, blank=True) objects = ProfileManager() + def send_instant_login_link(self): + token = str(uuid.uuid4()) + self.token = make_password(token) + self.save() + + path = reverse("hc-check-token", args=[self.user.username, token]) + ctx = {"login_link": settings.SITE_ROOT + path} + emails.login(self.user.email, ctx) + def send_report(self): # reset next report date first: now = timezone.now() diff --git a/hc/accounts/views.py b/hc/accounts/views.py index a92b1833..3eabd3bc 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -1,6 +1,5 @@ import uuid -from django.conf import settings from django.contrib import messages from django.contrib.auth import login as auth_login from django.contrib.auth import logout as auth_logout @@ -8,18 +7,17 @@ from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core import signing -from django.core.urlresolvers import reverse from django.http import HttpResponseBadRequest from django.shortcuts import redirect, render from hc.accounts.forms import EmailForm, ReportSettingsForm from hc.accounts.models import Profile from hc.api.models import Channel, Check -from hc.lib import emails def _make_user(email): username = str(uuid.uuid4())[:30] user = User(username=username, email=email) + user.set_unusable_password() user.save() channel = Channel() @@ -46,18 +44,6 @@ def _associate_demo_check(request, user): del request.session["welcome_code"] -def _send_login_link(user): - token = str(uuid.uuid4()) - user.set_password(token) - user.save() - - login_link = reverse("hc-check-token", args=[user.username, token]) - login_link = settings.SITE_ROOT + login_link - ctx = {"login_link": login_link} - - emails.login(user.email, ctx) - - def login(request): if request.method == 'POST': form = EmailForm(request.POST) @@ -69,12 +55,8 @@ def login(request): user = _make_user(email) _associate_demo_check(request, user) - # We don't want to reset passwords of staff users :-) - if user.is_staff: - return HttpResponseBadRequest() - - _send_login_link(user) - + profile = Profile.objects.for_user(user) + profile.send_instant_login_link() return redirect("hc-login-link-sent") else: @@ -99,17 +81,17 @@ def check_token(request, username, token): # User is already logged in return redirect("hc-checks") - user = authenticate(username=username, password=token) - if user is not None: - if user.is_active: - # This should get rid of "welcome_code" in session - request.session.flush() + user = authenticate(username=username, token=token) + if user is not None and user.is_active: + # This should get rid of "welcome_code" in session + request.session.flush() - user.set_unusable_password() - user.save() - auth_login(request, user) + profile = Profile.objects.for_user(user) + profile.token = "" + profile.save() + auth_login(request, user) - return redirect("hc-checks") + return redirect("hc-checks") request.session["bad_link"] = True return redirect("hc-login") diff --git a/hc/settings.py b/hc/settings.py index 0c44bf6f..7779fd42 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -51,6 +51,11 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', ) +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'hc.accounts.backends.ProfileBackend' +) + ROOT_URLCONF = 'hc.urls' TEMPLATES = [