Skip to content
Snippets Groups Projects
Commit 75d36cd0 authored by Julian's avatar Julian
Browse files

basic TOTP implementation, no integration yet

parent 21cebaa3
No related branches found
No related tags found
No related merge requests found
...@@ -39,10 +39,10 @@ def create_app(test_config=None): ...@@ -39,10 +39,10 @@ def create_app(test_config=None):
db.init_app(app) db.init_app(app)
# pylint: disable=C0415 # pylint: disable=C0415
from uffd import user, selfservice, role, mail, session, csrf, ldap from uffd import user, selfservice, role, mail, session, csrf, ldap, mfa
# pylint: enable=C0415 # pylint: enable=C0415
for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + ldap.bp: for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + ldap.bp + mfa.bp:
app.register_blueprint(i) app.register_blueprint(i)
@app.route("/") @app.route("/")
......
from .views import bp as _bp
bp = [_bp]
import enum
import datetime
import secrets, time, struct, hmac, hashlib, base64, urllib.parse
from flask import request, current_app
from sqlalchemy import Column, Integer, Enum, Boolean, String, DateTime
from uffd.database import db
from uffd.user.models import User
class MFAType(enum.Enum):
TOTP = 1
class MFAMethod(db.Model):
__tablename__ = 'mfa_method'
id = Column(Integer(), primary_key=True, autoincrement=True)
type = Column(Enum(MFAType))
created = Column(DateTime())
name = Column(String(128))
dn = Column(String(128))
__mapper_args__ = {
'polymorphic_on': type,
}
def __init__(self, user, name=None):
self.user = user
self.name = name
self.created = datetime.datetime.now();
@property
def user(self):
return User.from_ldap_dn(self.dn)
@user.setter
def user(self, u):
self.dn = u.dn
def _hotp(counter, key, digits=6):
'''Generates HMAC-based one-time password according to RFC4226
:param counter: Positive integer smaller than 2**64
:param key: Bytes object of arbitrary length (should be at least 160 bits)
:param digits: Length of resulting value (integer between 1 and 9, minimum
of 6 is recommended)
:returns: String object representing human-readable HOTP value'''
msg = struct.pack('>Q', counter)
digest = hmac.new(key, msg=msg, digestmod=hashlib.sha1).digest()
offset = digest[19] & 0x0f
snum = struct.unpack('>L', digest[offset:offset+4])[0] & 0x7fffffff
return str(snum % (10**digits)).zfill(digits)
class TOTPMethod(MFAMethod):
key = Column('totp_key', String(64))
__mapper_args__ = {
'polymorphic_identity': MFAType.TOTP
}
def __init__(self, user, name=None, key=None):
super().__init__(user, name)
if key is None:
key = base64.b32encode(secrets.token_bytes(16)).rstrip(b'=').decode()
self.key = key
@property
def raw_key(self):
s = self.key + '='*(8 - (len(self.key) % 8))
return base64.b32decode(s.encode())
@property
def key_uri(self):
issuer = urllib.parse.quote(urllib.parse.urlsplit(request.url).netloc)
accountname = urllib.parse.quote(self.user.loginname.encode())
params = {'secret': self.key, 'issuer': issuer}
if 'MFA_ICON_URL' in current_app.config:
params['image'] = current_app.config['MFA_ICON_URL']
return 'otpauth://totp/%s:%s?%s'%(issuer, accountname, urllib.parse.urlencode(params))
def verify(self, code):
'''Verify that code is valid
Code verification must be rate-limited!
:param code: String of digits (as entered by the user)
:returns: True if code is valid, False otherwise'''
counter = int(time.time()/30)
if _hotp(counter-1, self.raw_key) == code or _hotp(counter, self.raw_key) == code:
return True
return False
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("mfa.auth_finish", ref=ref) }}" method="POST">
<div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
<div class="text-center">
<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Two-Factor Authentication</h2>
</div>
<div class="form-group col-12">
<label for="mfa-code">Two-factor authentication code</label>
<input type="text" class="form-control" id="mfa-code" name="code" required="required" tabindex="1">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex="2">Verify</button>
</div>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div class="btn-toolbar">
<a class="btn btn-primary mb-2 ml-auto" href="{{ url_for('mfa.setup_totp') }}">Setup TOTP</a>
</div>
{% if methods %}
<table class="table">
<thead>
<tr>
<th scope="col" colspan=2>Name</th>
<th scope="col">Registered On</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for method in methods %}
<tr>
<td style="width: 0.5em;"><i class="fas fa-mobile-alt"></i></td>
<td>{{ method.name }}</td>
<td>{{ method.created }}</td>
<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info" role="alert">
You have not setup any two-factor methods yet!
</div>
{% endif %}
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div style="max-width: 75vh;" class="mx-auto">
{{ method.key_uri|qrcode_svg(width='100%', height='100%') }}
</div>
<form action="{{ url_for('mfa.setup_totp') }}" method="POST" class="form">
<div class="form-group">
<label for="code">Code</label>
<input name="code" type="text" class="form-control" required="required">
</div>
<div class="form-group">
<label for="name">Authenticator Name</label>
<input name="name" type="text" class="form-control" required="required">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-block">Verify</button>
</div>
</form>
{% endblock %}
from flask import Blueprint, render_template, session, request, redirect, url_for, flash
from uffd.database import db
from uffd.mfa.models import TOTPMethod
from uffd.session.views import get_current_user, login_required
bp = Blueprint('mfa', __name__, template_folder='templates', url_prefix='/mfa/')
@bp.route('/', methods=['GET'])
@login_required()
def setup():
user = get_current_user()
methods = TOTPMethod.query.filter_by(dn=user.dn).all()
return render_template('setup.html', methods=methods)
@bp.route('/setup/totp', methods=['GET'])
@login_required()
def setup_totp():
user = get_current_user()
method = TOTPMethod(user)
session['mfa_totp_key'] = method.key
return render_template('setup_totp.html', method=method)
@bp.route('/setup/totp', methods=['POST'])
@login_required()
def setup_totp_finish():
user = get_current_user()
method = TOTPMethod(user, name=request.form['name'], key=session['mfa_totp_key'])
del session['mfa_totp_key']
if method.verify(request.form['code']):
db.session.add(method)
db.session.commit()
return redirect(url_for('mfa.setup'))
flash('Code is invalid')
return redirect(url_for('mfa.setup_totp'))
@bp.route('/setup/totp/<int:id>/delete')
@login_required()
def delete_totp(id):
user = get_current_user()
method = TOTPMethod.query.filter_by(dn=user.dn, id=id).first_or_404()
db.session.delete(method)
db.session.commit()
return redirect(url_for('mfa.setup'))
@bp.route('/auth', methods=['GET'])
@login_required()
def auth():
user = get_current_user()
methods = TOTPMethod.query.filter_by(dn=user.dn).all()
return render_template('auth.html', ref=request.values.get('ref'), methods=methods)
@bp.route('/auth', methods=['POST'])
@login_required()
def auth_finish():
user = get_current_user()
methods = TOTPMethod.query.filter_by(dn=user.dn).all()
for method in methods:
if method.verify(request.form['code']):
session['mfa_verifed'] = True
return redirect(request.values.get('ref', url_for('index')))
flash('Two-factor authentication failed')
return redirect(url_for('mfa.auth', ref=request.values.get('ref')))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment