mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-08 14:40:27 +00:00
Add OTP authentication feature
This commit is contained in:
parent
af7402096e
commit
f4e2c3b3df
@ -1,10 +1,12 @@
|
||||
import os
|
||||
import os
|
||||
import ldap
|
||||
import time
|
||||
import base64
|
||||
import bcrypt
|
||||
import urlparse
|
||||
import itertools
|
||||
import traceback
|
||||
import onetimepass
|
||||
|
||||
from datetime import datetime
|
||||
from distutils.version import StrictVersion
|
||||
@ -51,9 +53,10 @@ class User(db.Model):
|
||||
lastname = db.Column(db.String(64))
|
||||
email = db.Column(db.String(128))
|
||||
avatar = db.Column(db.String(128))
|
||||
otp_secret = db.Column(db.String(16))
|
||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||
|
||||
def __init__(self, id=None, username=None, password=None, plain_text_password=None, firstname=None, lastname=None, role_id=None, email=None, avatar=None, reload_info=True):
|
||||
def __init__(self, id=None, username=None, password=None, plain_text_password=None, firstname=None, lastname=None, role_id=None, email=None, avatar=None, otp_secret=None, reload_info=True):
|
||||
self.id = id
|
||||
self.username = username
|
||||
self.password = password
|
||||
@ -63,6 +66,7 @@ class User(db.Model):
|
||||
self.role_id = role_id
|
||||
self.email = email
|
||||
self.avatar = avatar
|
||||
self.otp_secret = otp_secret
|
||||
|
||||
if reload_info:
|
||||
user_info = self.get_user_info_by_id() if id else self.get_user_info_by_username()
|
||||
@ -74,6 +78,7 @@ class User(db.Model):
|
||||
self.lastname = user_info.lastname
|
||||
self.email = user_info.email
|
||||
self.role_id = user_info.role_id
|
||||
self.otp_secret = user_info.otp_secret
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
@ -93,6 +98,12 @@ class User(db.Model):
|
||||
def __repr__(self):
|
||||
return '<User %r>' % (self.username)
|
||||
|
||||
def get_totp_uri(self):
|
||||
return 'otpauth://totp/PowerDNS-Admin:%s?secret=%s&issuer=PowerDNS-Admin' % (self.username, self.otp_secret)
|
||||
|
||||
def verify_totp(self, token):
|
||||
return onetimepass.valid_totp(token, self.otp_secret)
|
||||
|
||||
def get_hashed_password(self, plain_text_password=None):
|
||||
# Hash a password for the first time
|
||||
# (Using bcrypt, the salt is saved into the hash itself)
|
||||
@ -260,7 +271,7 @@ class User(db.Model):
|
||||
except Exception, e:
|
||||
raise
|
||||
|
||||
def update_profile(self):
|
||||
def update_profile(self, enable_otp=None):
|
||||
"""
|
||||
Update user profile
|
||||
"""
|
||||
@ -277,6 +288,16 @@ class User(db.Model):
|
||||
if self.avatar:
|
||||
user.avatar = self.avatar
|
||||
|
||||
if enable_otp == True:
|
||||
# generate the opt secret key
|
||||
user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')
|
||||
elif enable_otp == False:
|
||||
# set otp_secret="" means we want disable the otp authenticaion.
|
||||
user.otp_secret = ""
|
||||
else:
|
||||
# do nothing.
|
||||
pass
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
return True
|
||||
|
@ -55,6 +55,9 @@
|
||||
{% endif %}
|
||||
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
|
||||
</div>
|
||||
|
||||
{% if ldap_enabled and basic_enabled %}
|
||||
<div class="form-group">
|
||||
|
@ -31,6 +31,8 @@
|
||||
Avatar</a></li>
|
||||
<li><a href="#password_tab" data-toggle="tab">Change
|
||||
Password</a></li>
|
||||
<li><a href="#authentication_tab" data-toggle="tab">Authentication
|
||||
</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="personal_tab">
|
||||
@ -107,6 +109,20 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane" id="authentication_tab">
|
||||
<form action="{{ user_profile }}" method="post">
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="{{ current_user.username }}" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %}>
|
||||
<label for="otp_toggle">Enable Two Factor Authentication</label>
|
||||
{% if current_user.otp_secret %}
|
||||
<p><img id="qrcode" src="{{ url_for('qrcode') }}"></p>
|
||||
Please start FreeOTP (<a target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en">Android</a> - <a target="_blank" href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>) on your smartphone and scan the above QR Code with it.
|
||||
<br/>
|
||||
<font color="red"><strong><i>Make sure only you can see this QR Code and nobodoy can capture it.</i></strong></font>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,4 +133,25 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<!-- TODO: add password and password confirmation comparisson check -->
|
||||
<script>
|
||||
// initialize pretty checkboxes
|
||||
$('.otp_toggle').iCheck({
|
||||
checkboxClass : 'icheckbox_square-blue',
|
||||
increaseArea : '20%'
|
||||
});
|
||||
|
||||
// handle checkbox toggling
|
||||
$('.otp_toggle').on('ifToggled', function(event) {
|
||||
var enable_otp = $(this).prop('checked');
|
||||
var username = $(this).prop('id');
|
||||
postdata = {
|
||||
'action' : 'enable_otp',
|
||||
'data' : {
|
||||
'username' : username,
|
||||
'enable_otp' : enable_otp
|
||||
}
|
||||
};
|
||||
applyChanges(postdata, '/user/profile');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
40
app/views.py
40
app/views.py
@ -2,6 +2,7 @@ import os
|
||||
import json
|
||||
import jinja2
|
||||
import traceback
|
||||
import pyqrcode
|
||||
|
||||
from functools import wraps
|
||||
from flask.ext.login import login_user, logout_user, current_user, login_required
|
||||
@ -11,6 +12,8 @@ from werkzeug import secure_filename
|
||||
from lib import utils
|
||||
from app import app, login_manager
|
||||
from .models import User, Role, Domain, DomainUser, Record, Server, History, Anonymous, Setting
|
||||
|
||||
from io import BytesIO
|
||||
from distutils.util import strtobool
|
||||
|
||||
jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name
|
||||
@ -114,6 +117,7 @@ def login():
|
||||
# process login
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
otp_token = request.form['otptoken'] if 'otptoken' in request.form else None
|
||||
auth_method = request.form['auth_method'] if 'auth_method' in request.form else 'LOCAL'
|
||||
|
||||
# addition fields for registration case
|
||||
@ -138,6 +142,15 @@ def login():
|
||||
error = e.message['desc'] if 'desc' in e.message else e
|
||||
return render_template('login.html', error=error, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
|
||||
# check if user enabled OPT authentication
|
||||
if user.otp_secret:
|
||||
if otp_token:
|
||||
good_token = user.verify_totp(otp_token)
|
||||
if not good_token:
|
||||
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
else:
|
||||
return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
|
||||
login_user(user, remember = remember_me)
|
||||
return redirect(request.args.get('next') or url_for('index'))
|
||||
else:
|
||||
@ -542,6 +555,16 @@ def user_profile():
|
||||
email = request.form['email'] if 'email' in request.form else ''
|
||||
new_password = request.form['password'] if 'password' in request.form else ''
|
||||
|
||||
# json data
|
||||
if request.data:
|
||||
jdata = json.loads(request.data)
|
||||
data = jdata['data']
|
||||
if jdata['action'] == 'enable_otp':
|
||||
enable_otp = data['enable_otp']
|
||||
user = User(username=current_user.username)
|
||||
user.update_profile(enable_otp=enable_otp)
|
||||
return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: %s' % enable_otp } ), 200)
|
||||
|
||||
# get new avatar
|
||||
save_file_name = None
|
||||
if 'file' in request.files:
|
||||
@ -567,6 +590,23 @@ def user_avatar(filename):
|
||||
return send_from_directory(os.path.join(app.config['UPLOAD_DIR'], 'avatar'), filename)
|
||||
|
||||
|
||||
@app.route('/qrcode')
|
||||
@login_required
|
||||
def qrcode():
|
||||
if not current_user:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# render qrcode for FreeTOTP
|
||||
url = pyqrcode.create(current_user.get_totp_uri())
|
||||
stream = BytesIO()
|
||||
url.svg(stream, scale=3)
|
||||
return stream.getvalue(), 200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'}
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def index():
|
||||
|
@ -9,3 +9,5 @@ python-ldap==2.4.21
|
||||
Flask-SQLAlchemy==2.1
|
||||
SQLAlchemy==1.0.9
|
||||
sqlalchemy-migrate==0.10.0
|
||||
onetimepass==1.0.1
|
||||
PyQRCode==1.2
|
||||
|
Loading…
Reference in New Issue
Block a user