summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarian Sigler <m@qjym.de>2012-09-21 06:18:38 +0200
committerMarian Sigler <m@qjym.de>2012-09-21 06:18:38 +0200
commitec2c11c9b21f7fcbf79bdf2b57eff911d8c66bd9 (patch)
tree480fca283d4a37f45fa49d3bc66da48c8080e027
parent67dbc8ad19e6ee1cc7f919ea837dd497a7e15bf9 (diff)
downloadweb-ec2c11c9b21f7fcbf79bdf2b57eff911d8c66bd9.tar.gz
web-ec2c11c9b21f7fcbf79bdf2b57eff911d8c66bd9.tar.bz2
web-ec2c11c9b21f7fcbf79bdf2b57eff911d8c66bd9.zip
Add confirmation to registration.
First, only ask for username and email, then send out a confirmation mail. When the link therein is clicked, ask for a password and create the account in LDAP.
-rw-r--r--app.py53
-rw-r--r--forms.py4
-rw-r--r--templates/mail/register.txt19
-rw-r--r--templates/register.html7
-rw-r--r--templates/register_complete.html21
-rw-r--r--utils.py34
6 files changed, 115 insertions, 23 deletions
diff --git a/app.py b/app.py
index 22a11f2..4a18226 100644
--- a/app.py
+++ b/app.py
@@ -7,8 +7,8 @@ import account
import ldap
import os
from flask import flash, Flask, g, redirect, request, session, url_for
-from utils import templated, login_required, encrypt_password, decrypt_password, login_user, logout_user
-from forms import RegisterForm, LoginForm, SettingsForm
+from utils import *
+from forms import RegisterForm, RegisterCompleteForm, LoginForm, SettingsForm
app = Flask(__name__)
@@ -52,18 +52,55 @@ def register():
if request.method == 'POST' and form.validate():
username = form.username.data
mail = form.mail.data
+
+ confirm_token = make_confirmation('register', (username, mail))
+ confirm_link = url_for('register_complete', token=confirm_token, _external=True)
+
+ body = render_template('mail/register.txt', username=username,
+ mail=mail, link=confirm_link)
+
+ send_mail(mail, u'E-Mail-Adresse bestätigen', body,
+ sender=app.config.get('MAIL_CONFIRM_SENDER'))
+
+ flash(u'Es wurde eine E-Mail an die angegebene Adresse geschickt, '
+ u'um diese zu überprüfen. Bitte folge den Anweisungen in der '
+ u'E-Mail.')
+
+ return redirect(url_for('index'))
+
+ return {'form': form}
+
+
+@app.route('/register/<token>', methods=['GET', 'POST'])
+@templated('register_complete.html')
+def register_complete(token):
+ try:
+ username, mail = verify_confirmation('register', token.encode('ascii'), timeout=3*24*60*60)
+ except ConfirmationInvalid:
+ raise Forbidden(u'Ungültiger Bestätigungslink')
+ except ConfirmationTimeout:
+ raise Forbidden(u'Bestätigungslink ist zu alt')
+
+
+ form = RegisterCompleteForm(request.form)
+ if request.method == 'POST' and form.validate():
password = form.password.data
- user = Account(form.username.data, form.mail.data, password=form.password.data)
- service.register(user)
+ user = account.Account(username, mail, password=form.password.data)
+ g.ldap.register(user)
# populate request context and session
- assert login_user(user.username, user.password)
+ assert login_user(user.uid, user.password)
flash(u'Benutzer erfolgreich angelegt.')
- redirect(url_for('settings'))
-
- return {'form': form}
+ return redirect(url_for('settings'))
+
+ return {
+ 'form': form,
+ 'token': token,
+ 'username': username,
+ 'mail': mail,
+ }
diff --git a/forms.py b/forms.py
index 0feb375..22f77ea 100644
--- a/forms.py
+++ b/forms.py
@@ -6,10 +6,12 @@ username = TextField('Benutzername', [validators.Regexp(_username_re, message=u'
class RegisterForm(Form):
username = username
+ mail = TextField('E-Mail-Adresse', [validators.Email(), validators.Length(min=6, max=50)])
+
+class RegisterCompleteForm(Form):
password = PasswordField('Passwort', [validators.Required(),
validators.EqualTo('password_confirm', message=u'Passwörter stimmen nicht überein')])
password_confirm = PasswordField(u'Passwort bestätigen')
- mail = TextField('E-Mail-Adresse', [validators.Email(), validators.Length(min=6, max=50)])
class LoginForm(Form):
diff --git a/templates/mail/register.txt b/templates/mail/register.txt
new file mode 100644
index 0000000..1f837d6
--- /dev/null
+++ b/templates/mail/register.txt
@@ -0,0 +1,19 @@
+Hallo,
+
+Jemand, vermutlich du, möchte auf spline accounts [1] einen Account
+mit folgenden Daten anlegen:
+
+ Benutzername: {{ username }}
+ E-Mail-Adresse: {{ mail }}
+
+
+Wenn du diesen Account anlegen möchtest, bestätige mit folgendem Link
+deine E-Mail-Adresse:
+ <{{ link }}>
+
+
+Wenn du diesen Account nicht anlegen möchtest, brauchst du nichts
+weiter zu tun. Ohne deine Bestätigung wird der Account nicht erstellt.
+
+
+[1] {{ url_for('index', _external=True) }}
diff --git a/templates/register.html b/templates/register.html
index 658f8d1..e04baac 100644
--- a/templates/register.html
+++ b/templates/register.html
@@ -4,12 +4,11 @@
{%- block content %}
<form action="{{ url_for('register') }}" method="post">
<dl>
+ {{ form.errors }}
{{ render_field(form.username) }}
{{ render_field(form.mail) }}
- {{ render_field(form.password) }}
- {{ render_field(form.password_confirm) }}
</dl>
- {{ form.request_token }}
- <input type="submit" value="Registrieren" />
+ {{ form.csrf_token }}
+ <input type="submit" value="E-Mail-Adresse bestätigen" />
</form>
{%- endblock %}
diff --git a/templates/register_complete.html b/templates/register_complete.html
new file mode 100644
index 0000000..f44e43a
--- /dev/null
+++ b/templates/register_complete.html
@@ -0,0 +1,21 @@
+{%- extends 'base.html' %}
+{%- from '_macros.html' import render_field %}
+{%- set title = 'Account erstellen' %}
+{%- block content %}
+<form action="{{ url_for('register_complete', token=token) }}" method="post">
+ <p>
+ Deine E-Mail-Adresse wurde erfolgreich bestätigt.
+ Bitte setze nun ein Passwort, um die Registrierung abzuschließen.
+ </p>
+ <dl>
+ <dt>Benutzername</dt>
+ <dd><input readonly="readonly" value="{{ username }}" /></dd>
+ <dt>E-Mail-Adresse</dt>
+ <dd><input readonly="readonly" value="{{ mail }}" /></dd>
+ {{ render_field(form.password) }}
+ {{ render_field(form.password_confirm) }}
+ </dl>
+ {{ form.csrf_token }}
+ <input type="submit" value="Registrieren" />
+</form>
+{%- endblock %}
diff --git a/utils.py b/utils.py
index b749444..49a69d3 100644
--- a/utils.py
+++ b/utils.py
@@ -3,12 +3,16 @@ import hmac
import ldap
import pickle
import re
+import struct
+import subprocess
+from base64 import urlsafe_b64encode, urlsafe_b64decode
from Crypto.Cipher import AES
from email.mime.text import MIMEText
from functools import wraps
from flask import current_app, flash, g, redirect, render_template, request, session, url_for
from hashlib import sha1
from random import randint
+from time import time
from werkzeug.exceptions import Forbidden
@@ -86,7 +90,7 @@ def decrypt_password(ciphertext):
return encryptor.decrypt(ciphertext[16:]).rstrip('\0')
-def create_confirmation(realm, data):
+def make_confirmation(realm, data):
"""
Create a confirmation token e.g. for confirmation mails.
@@ -94,26 +98,36 @@ def create_confirmation(realm, data):
some data (pickle-able).
"""
key = '\0'.join((current_app.config['SECRET_KEY'], realm))
- payload = pickle.dumps(data)
+ payload = ''.join((struct.pack('>i', time()), pickle.dumps(data)))
mac = hmac.new(key, payload, sha1)
- return ''.join((mac.digest(), payload)).encode('base64').strip()
+ return urlsafe_b64encode(''.join((mac.digest(), payload))).rstrip('=')
-class InvalidConfirmation(ValueError):
+class ConfirmationInvalid(ValueError):
"""Raised by `verify_confirmation` on invalid input data"""
-def verify_confirmation(realm, token):
+class ConfirmationTimeout(ValueError):
+ """Raised by `verify_confirmation` when the input data is too old"""
+
+def verify_confirmation(realm, token, timeout=None):
"""
- Verify a confirmation created by `create_confirmation` and, if it is
+ Verify a confirmation token created by `make_confirmation` and, if it is
valid, return the original data.
+ If `timeout` is given, only accept the token if it is less than `timeout`
+ seconds old.
"""
key = '\0'.join((current_app.config['SECRET_KEY'], realm))
- token = token.decode('base64')
+ token = urlsafe_b64decode(token + '=' * (4 - len(token) % 4))
mac = token[:20]
- payload = token[20:]
+ tokentime = struct.unpack('>i', token[20:24])[0]
+ payload = token[24:]
+
+ if mac != hmac.new(key, token[20:], sha1).digest():
+ raise ConfirmationInvalid('MAC does not match')
- if mac != hmac.new(key, payload, sha1).digest():
- raise InvalidConfirmation('MAC does not match')
+ print '%d+%d=%d <> %d' % (tokentime, timeout, tokentime+timeout, time())
+ if timeout is not None and time() > tokentime + timeout:
+ raise ConfirmationTimeout('Token is too old')
return pickle.loads(payload)