diff options
-rw-r--r-- | app.py | 53 | ||||
-rw-r--r-- | forms.py | 4 | ||||
-rw-r--r-- | templates/mail/register.txt | 19 | ||||
-rw-r--r-- | templates/register.html | 7 | ||||
-rw-r--r-- | templates/register_complete.html | 21 | ||||
-rw-r--r-- | utils.py | 34 |
6 files changed, 115 insertions, 23 deletions
@@ -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, + } @@ -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 %} @@ -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) |