mirror of https://github.com/CIRCL/AIL-framework
chg: [2FA] user + admin manage 2FA OTP
parent
073181fbd8
commit
12e260a4d9
|
@ -167,10 +167,13 @@ def verify_user_totp(user_id, code):
|
|||
totp = _get_totp(get_user_otp_secret(user_id))
|
||||
return _verify_totp(totp, code)
|
||||
|
||||
def verify_user_hotp(user_id, code): # TODO IF valid increase counter
|
||||
def verify_user_hotp(user_id, code):
|
||||
hotp = _get_hotp(get_user_otp_secret(user_id))
|
||||
counter = get_user_hotp_counter(user_id)
|
||||
return _verify_hotp(hotp, counter, code)
|
||||
valid = _verify_hotp(hotp, counter, code)
|
||||
if valid:
|
||||
r_serv_db.hincrby(f'ail:user:metadata:{user_id}', 'otp_counter', 1)
|
||||
return valid
|
||||
|
||||
def verify_user_otp(user_id, code):
|
||||
if verify_user_totp(user_id, code):
|
||||
|
@ -298,6 +301,10 @@ class AILUser(UserMixin):
|
|||
meta['api_key'] = self.get_api_key()
|
||||
if 'role' in options:
|
||||
meta['role'] = get_user_role(self.user_id)
|
||||
if '2fa' in options:
|
||||
meta['2fa'] = self.is_2fa_enabled()
|
||||
if 'otp_setup' in options:
|
||||
meta['otp_setup'] = self.is_2fa_setup()
|
||||
return meta
|
||||
|
||||
## SESSION ##
|
||||
|
@ -381,7 +388,8 @@ class AILUser(UserMixin):
|
|||
def init_setup_2fa(self, create=True):
|
||||
if create:
|
||||
create_user_otp(self.user_id)
|
||||
return get_user_otp_qr_code(self.user_id, 'AIL TEST'), get_user_hotp_code(self.user_id)
|
||||
instance_name = 'AIL TEST'
|
||||
return get_user_otp_qr_code(self.user_id, instance_name), get_user_otp_uri(self.user_id, instance_name), get_user_hotp_code(self.user_id)
|
||||
|
||||
def setup_2fa(self):
|
||||
r_serv_db.hset(f'ail:user:metadata:{self.user_id}', 'otp_setup', 1)
|
||||
|
@ -418,20 +426,62 @@ class AILUser(UserMixin):
|
|||
|
||||
def api_get_users_meta():
|
||||
meta = {'users': []}
|
||||
options = {'api_key', 'role'}
|
||||
options = {'api_key', 'role', '2fa', 'otp_setup'}
|
||||
for user_id in get_users():
|
||||
user = AILUser(user_id)
|
||||
meta['users'].append(user.get_meta(options=options))
|
||||
return meta
|
||||
|
||||
def api_get_user_profile(user_id):
|
||||
options = {'api_key', 'role'}
|
||||
options = {'api_key', 'role', '2fa'}
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
return {'status': 'error', 'reason': 'User not found'}, 404
|
||||
meta = user.get_meta(options=options)
|
||||
return meta, 200
|
||||
|
||||
def api_get_user_hotp(user_id):
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
return {'status': 'error', 'reason': 'User not found'}, 404
|
||||
hotp = get_user_hotp_code(user_id)
|
||||
return hotp, 200
|
||||
|
||||
def api_enable_user_otp(user_id):
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
return {'status': 'error', 'reason': 'User not found'}, 404
|
||||
if user.is_2fa_enabled():
|
||||
return {'status': 'error', 'reason': 'User OTP is already setup'}, 400
|
||||
delete_user_otp(user_id)
|
||||
enable_user_2fa(user_id)
|
||||
return user_id, 200
|
||||
|
||||
def api_disable_user_otp(user_id):
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
return {'status': 'error', 'reason': 'User not found'}, 404
|
||||
if not user.is_2fa_enabled():
|
||||
return {'status': 'error', 'reason': 'User OTP is not enabled'}, 400
|
||||
if is_2fa_enabled():
|
||||
return {'status': 'error', 'reason': '2FA is enforced on this instance'}, 400
|
||||
disable_user_2fa(user_id)
|
||||
delete_user_otp(user_id)
|
||||
return user_id, 200
|
||||
|
||||
def api_reset_user_otp(admin_id, user_id):
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
return {'status': 'error', 'reason': 'User not found'}, 404
|
||||
admin = AILUser(admin_id)
|
||||
if not admin.is_in_role('admin'):
|
||||
return {'status': 'error', 'reason': 'Access Denied'}, 403
|
||||
if not user.is_2fa_setup():
|
||||
return {'status': 'error', 'reason': 'User OTP is not setup'}, 400
|
||||
delete_user_otp(user_id)
|
||||
enable_user_2fa(user_id)
|
||||
return user_id, 200
|
||||
|
||||
def api_create_user_api_key_self(user_id): # TODO LOG USER ID
|
||||
user = AILUser(user_id)
|
||||
if not user.exists():
|
||||
|
@ -471,7 +521,7 @@ def get_users_metadata(list_users):
|
|||
users.append(get_user_metadata(user))
|
||||
return users
|
||||
|
||||
def create_user(user_id, password=None, chg_passwd=True, role=None): # TODO ###############################################################
|
||||
def create_user(user_id, password=None, chg_passwd=True, role=None, otp=False): # TODO ###############################################################
|
||||
# # TODO: check password strength
|
||||
if password:
|
||||
new_password = password
|
||||
|
@ -482,7 +532,7 @@ def create_user(user_id, password=None, chg_passwd=True, role=None): # TODO ####
|
|||
# EDIT
|
||||
if exists_user(user_id):
|
||||
if password or chg_passwd:
|
||||
edit_user_password(user_id, password_hash, chg_passwd=chg_passwd)
|
||||
edit_user(user_id, password_hash, chg_passwd=chg_passwd)
|
||||
if role:
|
||||
edit_user_role(user_id, role)
|
||||
# CREATE USER
|
||||
|
@ -503,7 +553,10 @@ def create_user(user_id, password=None, chg_passwd=True, role=None): # TODO ####
|
|||
# create user token
|
||||
generate_new_token(user_id)
|
||||
|
||||
def edit_user_password(user_id, password_hash, chg_passwd=False):
|
||||
if otp or is_2fa_enabled():
|
||||
enable_user_2fa(user_id)
|
||||
|
||||
def edit_user(user_id, password_hash, chg_passwd=False, otp=False): # TODO ######################################################3333
|
||||
if chg_passwd:
|
||||
r_serv_db.hset(f'ail:user:metadata:{user_id}', 'change_passwd', 'True')
|
||||
else:
|
||||
|
@ -517,6 +570,11 @@ def edit_user_password(user_id, password_hash, chg_passwd=False):
|
|||
# create new token
|
||||
generate_new_token(user_id)
|
||||
|
||||
if otp or is_2fa_enabled():
|
||||
enable_user_2fa(user_id)
|
||||
else:
|
||||
disable_user_2fa(user_id)
|
||||
|
||||
# # TODO: solve edge_case self delete
|
||||
def delete_user(user_id):
|
||||
if exists_user(user_id):
|
||||
|
|
|
@ -87,8 +87,10 @@ def login():
|
|||
if not user.is_2fa_setup():
|
||||
return redirect(url_for('root.setup_2fa'))
|
||||
else:
|
||||
htop_counter = user.get_htop_counter()
|
||||
return redirect(url_for('root.verify_2fa', htop_counter=htop_counter))
|
||||
if next_page and next_page != 'None' and next_page != '/':
|
||||
return redirect(url_for('root.verify_2fa', next=next_page))
|
||||
else:
|
||||
return redirect(url_for('root.verify_2fa'))
|
||||
|
||||
else:
|
||||
# Login User
|
||||
|
@ -152,6 +154,7 @@ def verify_2fa():
|
|||
if request.method == 'POST':
|
||||
|
||||
code = request.form.get('otp')
|
||||
next_page = request.form.get('next_page')
|
||||
|
||||
if user.is_valid_otp(code):
|
||||
session.pop('user_id', None)
|
||||
|
@ -164,20 +167,19 @@ def verify_2fa():
|
|||
if user.request_password_change():
|
||||
return redirect(url_for('root.change_password'))
|
||||
else:
|
||||
# # next page
|
||||
# if next_page and next_page != 'None' and next_page != '/':
|
||||
# return redirect(next_page)
|
||||
# dashboard
|
||||
# else:
|
||||
# NEXT PAGE
|
||||
if next_page and next_page != 'None' and next_page != '/':
|
||||
return redirect(next_page)
|
||||
return redirect(url_for('dashboard.index'))
|
||||
else:
|
||||
htop_counter = user.get_htop_counter()
|
||||
error = "The OTP is incorrect or has expired"
|
||||
return render_template("verify_otp.html", htop_counter=htop_counter, error=error)
|
||||
return render_template("verify_otp.html", htop_counter=htop_counter, next_page=next_page, error=error)
|
||||
|
||||
else:
|
||||
htop_counter = user.get_htop_counter()
|
||||
return render_template("verify_otp.html", htop_counter=htop_counter)
|
||||
next_page = request.args.get('next')
|
||||
return render_template("verify_otp.html", htop_counter=htop_counter, next_page=next_page)
|
||||
|
||||
@root.route('/2fa/setup', methods=['POST', 'GET'])
|
||||
def setup_2fa():
|
||||
|
@ -222,10 +224,10 @@ def setup_2fa():
|
|||
else:
|
||||
error = request.args.get('error')
|
||||
if error:
|
||||
qr_code, hotp_codes = user.init_setup_2fa(create=False)
|
||||
qr_code, otp_url, hotp_codes = user.init_setup_2fa(create=False)
|
||||
else:
|
||||
qr_code, hotp_codes = user.init_setup_2fa()
|
||||
return render_template("setup_otp.html", qr_code=qr_code, hotp_codes=hotp_codes, error=error)
|
||||
qr_code, otp_url, hotp_codes = user.init_setup_2fa()
|
||||
return render_template("setup_otp.html", qr_code=qr_code, hotp_codes=hotp_codes, otp_url=otp_url, error=error)
|
||||
|
||||
@root.route('/change_password', methods=['POST', 'GET'])
|
||||
@login_required
|
||||
|
|
|
@ -70,7 +70,94 @@ def user_profile():
|
|||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
meta = r[0]
|
||||
return render_template("user_profile.html", meta=meta, acl_admin=acl_admin)
|
||||
global_2fa = ail_users.is_2fa_enabled()
|
||||
return render_template("user_profile.html", meta=meta, global_2fa=global_2fa,acl_admin=acl_admin)
|
||||
|
||||
@settings_b.route("/settings/user/hotp", methods=['GET'])
|
||||
@login_required
|
||||
@login_read_only
|
||||
def user_hotp():
|
||||
# if not current_user.is_authenticated: # TODO CHECK IF FRESH LOGIN/SESSION -> check last loging time -> rerequest if expired
|
||||
|
||||
acl_admin = current_user.is_in_role('admin')
|
||||
user_id = current_user.get_user_id()
|
||||
r = ail_users.api_get_user_hotp(user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
hotp = r[0]
|
||||
return render_template("user_hotp.html", hotp=hotp, acl_admin=acl_admin)
|
||||
|
||||
@settings_b.route("/settings/user/otp/enable/self", methods=['GET'])
|
||||
@login_required
|
||||
@login_read_only
|
||||
def user_otp_enable_self():
|
||||
user_id = current_user.get_user_id()
|
||||
r = ail_users.api_enable_user_otp(user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
current_user.kill_session()
|
||||
return redirect(url_for('settings_b.user_profile'))
|
||||
|
||||
@settings_b.route("/settings/user/otp/disable/self", methods=['GET'])
|
||||
@login_required
|
||||
@login_read_only
|
||||
def user_otp_disable_self():
|
||||
user_id = current_user.get_user_id()
|
||||
r = ail_users.api_disable_user_otp(user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
current_user.kill_session()
|
||||
return redirect(url_for('settings_b.user_profile'))
|
||||
|
||||
@settings_b.route("/settings/user/otp/reset/self", methods=['GET'])
|
||||
@login_required
|
||||
@login_admin
|
||||
def user_otp_reset_self(): # TODO ask for password ?
|
||||
user_id = current_user.get_user_id()
|
||||
r = ail_users.api_reset_user_otp(user_id, user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
else:
|
||||
current_user.kill_session()
|
||||
return redirect(url_for('settings_b.user_profile'))
|
||||
|
||||
@settings_b.route("/settings/user/otp/enable", methods=['GET'])
|
||||
@login_required
|
||||
@login_admin
|
||||
def user_otp_enable():
|
||||
user_id = request.args.get('user_id')
|
||||
r = ail_users.api_enable_user_otp(user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
user = ail_users.AILUser.get(user_id)
|
||||
user.kill_session()
|
||||
return redirect(url_for('settings_b.users_list'))
|
||||
|
||||
@settings_b.route("/settings/user/otp/disable", methods=['GET'])
|
||||
@login_required
|
||||
@login_admin
|
||||
def user_otp_disable():
|
||||
user_id = request.args.get('user_id')
|
||||
r = ail_users.api_disable_user_otp(user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
user = ail_users.AILUser.get(user_id)
|
||||
user.kill_session()
|
||||
return redirect(url_for('settings_b.users_list'))
|
||||
|
||||
@settings_b.route("/settings/user/otp/reset", methods=['GET'])
|
||||
@login_required
|
||||
@login_admin
|
||||
def user_otp_reset(): # TODO ask for password ?
|
||||
user_id = request.args.get('user_id')
|
||||
admin_id = current_user.get_user_id()
|
||||
r = ail_users.api_reset_user_otp(admin_id, user_id)
|
||||
if r[1] != 200:
|
||||
return create_json_response(r[0], r[1])
|
||||
else:
|
||||
user = ail_users.AILUser.get(user_id)
|
||||
user.kill_session()
|
||||
return redirect(url_for('settings_b.user_profile'))
|
||||
|
||||
@settings_b.route("/settings/user/api_key/new", methods=['GET'])
|
||||
@login_required
|
||||
|
@ -132,6 +219,11 @@ def create_user_post():
|
|||
role = request.form.get('user_role')
|
||||
password1 = request.form.get('password1')
|
||||
password2 = request.form.get('password2')
|
||||
enable_2_fa = request.form.get('enable_2_fa')
|
||||
if enable_2_fa or ail_users.is_2fa_enabled():
|
||||
enable_2_fa = True
|
||||
else:
|
||||
enable_2_fa = False
|
||||
|
||||
all_roles = ail_users.get_all_roles()
|
||||
|
||||
|
@ -156,14 +248,14 @@ def create_user_post():
|
|||
if not password1 and not password2:
|
||||
password = None
|
||||
str_password = 'Password not changed'
|
||||
ail_users.create_user(email, password=password, role=role)
|
||||
new_user = {'email': email, 'password': str_password}
|
||||
ail_users.create_user(email, password=password, role=role, otp=enable_2_fa)
|
||||
new_user = {'email': email, 'password': str_password, 'otp': enable_2_fa}
|
||||
return render_template("create_user.html", new_user=new_user, meta={}, all_roles=all_roles, acl_admin=True)
|
||||
|
||||
else:
|
||||
return render_template("create_user.html", all_roles=all_roles, acl_admin=True)
|
||||
return render_template("create_user.html", all_roles=all_roles, meta={}, acl_admin=True)
|
||||
else:
|
||||
return render_template("create_user.html", all_roles=all_roles, error_mail=True, acl_admin=True)
|
||||
return render_template("create_user.html", all_roles=all_roles, meta={}, error_mail=True, acl_admin=True)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -65,6 +65,11 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div class="custom-control custom-switch mt-4 mb-3">
|
||||
<input type="checkbox" class="custom-control-input" id="enable_2_fa" name="enable_2_fa" checked>
|
||||
<label class="custom-control-label" for="enable_2_fa">2FA OTP</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-control custom-switch mt-4 mb-3">
|
||||
<input type="checkbox" class="custom-control-input" id="set_manual_password" value="" onclick="toggle_password_fields();">
|
||||
<label class="custom-control-label" for="set_manual_password">{% if meta['id'] %}Reset{% else %}Set{% endif %} Password</label>
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>User Profile - AIL</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='image/ail-icon.png') }}">
|
||||
|
||||
<!-- Core CSS -->
|
||||
<link href="{{ url_for('static', filename='css/bootstrap4.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/dataTables.bootstrap.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- JS -->
|
||||
<script src="{{ url_for('static', filename='js/jquery.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/popper.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bootstrap4.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='js/dataTables.bootstrap.min.js')}}"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include 'nav_bar.html' %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
{% include 'settings/menu_sidebar.html' %}
|
||||
|
||||
<div class="col-12 col-lg-10" id="core_content">
|
||||
|
||||
<h1 class="text-center mt-2">HOTP - Paper-Based Single Use Tokens</h1>
|
||||
|
||||
<div>In case you don't have access to your phone or authentication software, please use the following list of tokens.</div>
|
||||
<div>Be sure to print these tokens and keep them in a secure place for future use.</div>
|
||||
|
||||
<div class="text-center my-4">
|
||||
{% for code in hotp %}
|
||||
<div><i>{{ code[:-6] }}</i> <b>{{ code[-6:] }}</b></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$("#nav_edit_profile").addClass("active");
|
||||
//$("#nav_my_profile").removeClass("text-muted");
|
||||
} );
|
||||
|
||||
function toggle_sidebar(){
|
||||
if($('#nav_menu').is(':visible')){
|
||||
$('#nav_menu').hide();
|
||||
$('#side_menu').removeClass('border-right')
|
||||
$('#side_menu').removeClass('col-lg-2')
|
||||
$('#core_content').removeClass('col-lg-10')
|
||||
}else{
|
||||
$('#nav_menu').show();
|
||||
$('#side_menu').addClass('border-right')
|
||||
$('#side_menu').addClass('col-lg-2')
|
||||
$('#core_content').addClass('col-lg-10')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
|
@ -62,6 +62,35 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2FA OTP</td>
|
||||
<td>
|
||||
{% if meta['2fa'] %}
|
||||
<span class="badge badge-success" style="font-size: 1.0rem;"><b>YES</b></span>
|
||||
{% if acl_admin %}
|
||||
<div>
|
||||
<a class="btn btn-sm btn-danger mt-1" href="{{url_for('settings_b.user_otp_reset_self')}}"><i class="fas fa-sync"></i> RESET OTP</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not global_2fa %}
|
||||
<div>
|
||||
<a class="btn btn-sm btn-danger mt-1" href="{{url_for('settings_b.user_otp_disable_self')}}"><i class="fas fa-warning"></i> DISABLE 2FA OTP</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-danger" style="font-size: 1.0rem;"><b>NO</b></span>
|
||||
<div>
|
||||
<a class="btn btn-success" href="{{url_for('settings_b.user_otp_enable_self')}}"><i class="fas fa-plus"></i> ACTIVATE 2FA OTP</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if meta['2fa'] %}
|
||||
<tr>
|
||||
<td>TOTP</td>
|
||||
<td><a class="btn btn-info" href="{{url_for('settings_b.user_hotp')}}"><i class="fas fa-eye"></i> View Paper Tokens</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
<th>Role</th>
|
||||
<th>Api Key</th>
|
||||
<th></th>
|
||||
<th>2FA</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -45,7 +46,7 @@
|
|||
<td>{{user['role']}}</td>
|
||||
<td>
|
||||
<span id="censored_key_{{loop.index0}}">
|
||||
{{user['api_key'][:4]}}*********************************{{user['api_key'][-4:]}}
|
||||
{{user['api_key'][:4]}}**...**{{user['api_key'][-4:]}}
|
||||
</span>
|
||||
<span id="uncensored_key_{{loop.index0}}" style="display: none;">
|
||||
{{user['api_key']}}
|
||||
|
@ -59,6 +60,27 @@
|
|||
<i class="fas fa-eye"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if user['2fa'] %}
|
||||
{% if user['otp_setup'] %}
|
||||
<span class="badge badge-success" style="font-size: 1.0rem;"><b>YES</b></span>
|
||||
<a class="btn btn-outline-danger px-1 py-0" href="{{ url_for('settings_b.user_otp_reset', user_id=user['id']) }}">
|
||||
<i class="fas fa-random"></i> Reset
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge badge-warning" style="font-size: 1.0rem;"><b>ENFORCED</b></span>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-danger px-1 py-0" href="{{ url_for('settings_b.user_otp_disable', user_id=user['id']) }}">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge badge-danger" style="font-size: 1.0rem;"><b>NO</b></span>
|
||||
<a class="btn btn-outline-success px-1 py-0" href="{{ url_for('settings_b.user_otp_enable', user_id=user['id']) }}">
|
||||
<i class="fas fa-plus"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-start">
|
||||
<a class="btn btn-outline-primary ml-3 px-1 py-0" href="{{ url_for('settings_b.edit_user', user_id=user['id']) }}">
|
||||
|
|
|
@ -21,13 +21,23 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<h3 class="text-secondary">TOTP</h3>
|
||||
<h3 class="text-secondary mb-0"><b>TOTP</b></h3>
|
||||
<img src="data:image/png;base64, {{ qr_code }}">
|
||||
<div style="font-size: 20px;"> - Install an <b>authenticator application</b> on your mobile.</div>
|
||||
<div style="font-size: 20px;"> - <b>Scan</b> the QRCode</div>
|
||||
<p>
|
||||
<button class="btn btn-sm btn-info" type="button" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
|
||||
<i class="fas fa-link"></i> <b>TOTP URL</b>
|
||||
</button>
|
||||
</p>
|
||||
<div class="collapse" id="collapseExample">
|
||||
<div class="card card-body">
|
||||
{{ otp_url }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<h3 class="text-secondary">HOTP</h3>
|
||||
<h3 class="text-secondary"><b>HOTP</b></h3>
|
||||
{% for code in hotp_codes %}
|
||||
<div><i>{{ code[:-6] }}</i> <b>{{ code[-6:] }}</b></div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -68,10 +68,10 @@
|
|||
|
||||
<form class="form-signin" action="{{ url_for('root.verify_2fa')}}" method="post">
|
||||
<img class="mb-4" src="{{ url_for('static', filename='image/ail-project.png')}}" width="300">
|
||||
<h1 class="h3 mb-3 text-secondary">2FA: Please Enter your TOTP or HOTP</h1>
|
||||
<h1 class="h3 mb-3 text-secondary">2FA: Please Enter your TOTP or HOTP #{{ htop_counter }}</h1>
|
||||
{# <label for="inputEmail" class="sr-only">Email address</label>#}
|
||||
<input type="text" id="otp" name="otp" class="form-control {% if error %}is-invalid{% endif %}" placeholder="OTP Code" autocomplete="off" required autofocus>
|
||||
{# <input type="text" id="next_page" name="next_page" value="{{next_page}}" hidden>#}
|
||||
<input type="text" id="next_page" name="next_page" value="{{next_page}}" hidden>
|
||||
{% if error %}
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
|
|
Loading…
Reference in New Issue