mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2025-01-07 19:05:39 +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 ldap
|
||||||
import time
|
import time
|
||||||
|
import base64
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import urlparse
|
import urlparse
|
||||||
import itertools
|
import itertools
|
||||||
import traceback
|
import traceback
|
||||||
|
import onetimepass
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
@ -51,9 +53,10 @@ class User(db.Model):
|
|||||||
lastname = db.Column(db.String(64))
|
lastname = db.Column(db.String(64))
|
||||||
email = db.Column(db.String(128))
|
email = db.Column(db.String(128))
|
||||||
avatar = 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'))
|
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.id = id
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
@ -63,6 +66,7 @@ class User(db.Model):
|
|||||||
self.role_id = role_id
|
self.role_id = role_id
|
||||||
self.email = email
|
self.email = email
|
||||||
self.avatar = avatar
|
self.avatar = avatar
|
||||||
|
self.otp_secret = otp_secret
|
||||||
|
|
||||||
if reload_info:
|
if reload_info:
|
||||||
user_info = self.get_user_info_by_id() if id else self.get_user_info_by_username()
|
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.lastname = user_info.lastname
|
||||||
self.email = user_info.email
|
self.email = user_info.email
|
||||||
self.role_id = user_info.role_id
|
self.role_id = user_info.role_id
|
||||||
|
self.otp_secret = user_info.otp_secret
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
return True
|
return True
|
||||||
@ -93,6 +98,12 @@ class User(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<User %r>' % (self.username)
|
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):
|
def get_hashed_password(self, plain_text_password=None):
|
||||||
# Hash a password for the first time
|
# Hash a password for the first time
|
||||||
# (Using bcrypt, the salt is saved into the hash itself)
|
# (Using bcrypt, the salt is saved into the hash itself)
|
||||||
@ -260,7 +271,7 @@ class User(db.Model):
|
|||||||
except Exception, e:
|
except Exception, e:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def update_profile(self):
|
def update_profile(self, enable_otp=None):
|
||||||
"""
|
"""
|
||||||
Update user profile
|
Update user profile
|
||||||
"""
|
"""
|
||||||
@ -277,6 +288,16 @@ class User(db.Model):
|
|||||||
if self.avatar:
|
if self.avatar:
|
||||||
user.avatar = 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:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
|
@ -55,6 +55,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if ldap_enabled and basic_enabled %}
|
{% if ldap_enabled and basic_enabled %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
Avatar</a></li>
|
Avatar</a></li>
|
||||||
<li><a href="#password_tab" data-toggle="tab">Change
|
<li><a href="#password_tab" data-toggle="tab">Change
|
||||||
Password</a></li>
|
Password</a></li>
|
||||||
|
<li><a href="#authentication_tab" data-toggle="tab">Authentication
|
||||||
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane active" id="personal_tab">
|
<div class="tab-pane active" id="personal_tab">
|
||||||
@ -107,6 +109,20 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,4 +133,25 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extrascripts %}
|
{% block extrascripts %}
|
||||||
<!-- TODO: add password and password confirmation comparisson check -->
|
<!-- 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 %}
|
{% endblock %}
|
||||||
|
40
app/views.py
40
app/views.py
@ -2,6 +2,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import jinja2
|
import jinja2
|
||||||
import traceback
|
import traceback
|
||||||
|
import pyqrcode
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask.ext.login import login_user, logout_user, current_user, login_required
|
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 lib import utils
|
||||||
from app import app, login_manager
|
from app import app, login_manager
|
||||||
from .models import User, Role, Domain, DomainUser, Record, Server, History, Anonymous, Setting
|
from .models import User, Role, Domain, DomainUser, Record, Server, History, Anonymous, Setting
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
|
||||||
jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name
|
jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name
|
||||||
@ -114,6 +117,7 @@ def login():
|
|||||||
# process login
|
# process login
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = request.form['password']
|
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'
|
auth_method = request.form['auth_method'] if 'auth_method' in request.form else 'LOCAL'
|
||||||
|
|
||||||
# addition fields for registration case
|
# addition fields for registration case
|
||||||
@ -138,6 +142,15 @@ def login():
|
|||||||
error = e.message['desc'] if 'desc' in e.message else e
|
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)
|
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)
|
login_user(user, remember = remember_me)
|
||||||
return redirect(request.args.get('next') or url_for('index'))
|
return redirect(request.args.get('next') or url_for('index'))
|
||||||
else:
|
else:
|
||||||
@ -542,6 +555,16 @@ def user_profile():
|
|||||||
email = request.form['email'] if 'email' in request.form else ''
|
email = request.form['email'] if 'email' in request.form else ''
|
||||||
new_password = request.form['password'] if 'password' 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
|
# get new avatar
|
||||||
save_file_name = None
|
save_file_name = None
|
||||||
if 'file' in request.files:
|
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)
|
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'])
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
@ -9,3 +9,5 @@ python-ldap==2.4.21
|
|||||||
Flask-SQLAlchemy==2.1
|
Flask-SQLAlchemy==2.1
|
||||||
SQLAlchemy==1.0.9
|
SQLAlchemy==1.0.9
|
||||||
sqlalchemy-migrate==0.10.0
|
sqlalchemy-migrate==0.10.0
|
||||||
|
onetimepass==1.0.1
|
||||||
|
PyQRCode==1.2
|
||||||
|
Loading…
Reference in New Issue
Block a user