From 12e260a4d91e7dc2bf9add09ee588250fcd4908d Mon Sep 17 00:00:00 2001 From: terrtia Date: Thu, 27 Jun 2024 17:06:05 +0200 Subject: [PATCH] chg: [2FA] user + admin manage 2FA OTP --- bin/lib/ail_users.py | 74 ++++++++++++-- var/www/blueprints/root.py | 26 ++--- var/www/blueprints/settings_b.py | 102 ++++++++++++++++++- var/www/templates/settings/create_user.html | 5 + var/www/templates/settings/user_hotp.html | 68 +++++++++++++ var/www/templates/settings/user_profile.html | 29 ++++++ var/www/templates/settings/users_list.html | 24 ++++- var/www/templates/setup_otp.html | 16 ++- var/www/templates/verify_otp.html | 4 +- 9 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 var/www/templates/settings/user_hotp.html diff --git a/bin/lib/ail_users.py b/bin/lib/ail_users.py index e3eb4434..221e912b 100755 --- a/bin/lib/ail_users.py +++ b/bin/lib/ail_users.py @@ -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): diff --git a/var/www/blueprints/root.py b/var/www/blueprints/root.py index a705fc38..76403afe 100644 --- a/var/www/blueprints/root.py +++ b/var/www/blueprints/root.py @@ -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 diff --git a/var/www/blueprints/settings_b.py b/var/www/blueprints/settings_b.py index dc94a1e2..af7a3fc8 100644 --- a/var/www/blueprints/settings_b.py +++ b/var/www/blueprints/settings_b.py @@ -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) diff --git a/var/www/templates/settings/create_user.html b/var/www/templates/settings/create_user.html index b1e1daaa..c5f0432c 100644 --- a/var/www/templates/settings/create_user.html +++ b/var/www/templates/settings/create_user.html @@ -65,6 +65,11 @@ {% endfor %} +
+ + +
+
diff --git a/var/www/templates/settings/user_hotp.html b/var/www/templates/settings/user_hotp.html new file mode 100644 index 00000000..b24b74f9 --- /dev/null +++ b/var/www/templates/settings/user_hotp.html @@ -0,0 +1,68 @@ + + + + + User Profile - AIL + + + + + + + + + + + + + + + + + +{% include 'nav_bar.html' %} +
+
+ {% include 'settings/menu_sidebar.html' %} + +
+ +

HOTP - Paper-Based Single Use Tokens

+ +
In case you don't have access to your phone or authentication software, please use the following list of tokens.
+
Be sure to print these tokens and keep them in a secure place for future use.
+ +
+ {% for code in hotp %} +
{{ code[:-6] }} {{ code[-6:] }}
+ {% endfor %} +
+ +
+
+
+ + + + + diff --git a/var/www/templates/settings/user_profile.html b/var/www/templates/settings/user_profile.html index cdf8745b..41a10f2e 100644 --- a/var/www/templates/settings/user_profile.html +++ b/var/www/templates/settings/user_profile.html @@ -62,6 +62,35 @@ + + 2FA OTP + + {% if meta['2fa'] %} + YES + {% if acl_admin %} +
+ RESET OTP +
+ {% endif %} + {% if not global_2fa %} +
+ DISABLE 2FA OTP +
+ {% endif %} + {% else %} + NO +
+ ACTIVATE 2FA OTP +
+ {% endif %} + + + {% if meta['2fa'] %} + + TOTP + View Paper Tokens + + {% endif %}
diff --git a/var/www/templates/settings/users_list.html b/var/www/templates/settings/users_list.html index 54fe8eaa..9eb1c165 100644 --- a/var/www/templates/settings/users_list.html +++ b/var/www/templates/settings/users_list.html @@ -35,6 +35,7 @@ Role Api Key + 2FA Actions @@ -45,7 +46,7 @@ {{user['role']}} - {{user['api_key'][:4]}}*********************************{{user['api_key'][-4:]}} + {{user['api_key'][:4]}}**...**{{user['api_key'][-4:]}} + + {% if user['2fa'] %} + {% if user['otp_setup'] %} + YES + + Reset + + {% else %} + ENFORCED + {% endif %} + + + + {% else %} + NO + + + + {% endif %} + +
diff --git a/var/www/templates/setup_otp.html b/var/www/templates/setup_otp.html index e2548b60..c3ccc793 100644 --- a/var/www/templates/setup_otp.html +++ b/var/www/templates/setup_otp.html @@ -21,13 +21,23 @@
-

TOTP

+

TOTP

- Install an authenticator application on your mobile.
- Scan the QRCode
-
+

+ +

+
+
+ {{ otp_url }} +
+
+
-

HOTP

+

HOTP

{% for code in hotp_codes %}
{{ code[:-6] }} {{ code[-6:] }}
{% endfor %} diff --git a/var/www/templates/verify_otp.html b/var/www/templates/verify_otp.html index bc27df04..e936fa94 100644 --- a/var/www/templates/verify_otp.html +++ b/var/www/templates/verify_otp.html @@ -68,10 +68,10 @@