Add 2FA support

This commit is contained in:
kitoy 2025-11-05 20:10:01 +01:00
parent 12669a86fa
commit f9b092e456
4 changed files with 132 additions and 33 deletions

View File

@ -25,10 +25,10 @@
<p class="lead">
<form method="POST" action="{{ url_for('loginlogout.login') }}">
<input type="text" name="user" id="user" placeholder="Utilisateur" class="form-control" width="200px"><br />
<input type="password" name="passwd" id="passwd" placeholder="Mot de passe" class="form-control"><br />
<input type="text" name="user" id="user" placeholder="Utilisateur" width="200px"><br />
<input type="password" name="passwd" id="passwd" placeholder="Mot de passe"><br />
<input type="checkbox" id="totpcheckbox">
<label for="totpcheckbox">J'ai un code d'authentification 2FA</label>
<label for="totpcheckbox">J'ai un mot de passe à usage unique</label>
<br />
<br />
<label for="2FAInput" class="totp">Code TOTP:</label>

48
templates/mypassword.html Normal file
View File

@ -0,0 +1,48 @@
{% extends 'up_squelette.html' %}
{% block main %}
<h3> Changer mon mot de passe </h3>
<form method="POST" action="" >
<p> Votre compte sur ce serveur : {{ username }} </p>
<label> Mot de passe </label>
<input type="password" name="password" id="password" placeholder="Votre mot de passe"><br />
<input type="password" name="passwd_confirm" id="passwd_confirm" placeholder="Confirmation du mot de passe"><br />
<button class="btn btn-default btn-primary" type="submit">Envoyer</button>
</form>
<h3> Mot de passe à usage unique </h3>
<p> Le mot de passe à usage unique utilisé par pywallter est un mot de passe composé de chiffres qui change périodiquement (ttes les 30 sec). Cela permet de lutter contres le phishing et le vol de votre de mot de passe. <br/> En cas de vol de votre mot de passe une application génère un code à usage unique et donc le voleur ne peut pas se connecter à votre compte juste en connaissant votre de passe </p>
<p> Voici quelques exemple de générateur de mot de passe à usage unique (OTP en anglais) pour <a href="https://getaegis.app/">Android</a>, <a href="https://apps.apple.com/us/app/totp-authenticator-fast-2fa/id1404230533">iOS</a> et <a href="https://keepassxc.org/">Linux/BSD et windows</a>
<p> Afin que le serveur et votre application génère le même code en même temps; il vous faut valider la clef sécrete partager par votre application et le serveur. Pour cela, si ce n'est pas déjà fait, scannez ou entrez la clef secrète dans votre application. Pour finir, cliquez sur valider la clef en entrant le code générer par votre application. Si le test réussi parfait c'est configuré ! </p>
<p> Pour changer votre clef secrète vous devez d'abord supprimer votre clef actuelle pour en regénérer une nouvelle. </p>
<br/>
<p> Votre clef secrète : {{ totp_shared_key }} </p>
{% if shared_key_validate %}
<p class="success"> Votre clef secrète est valide et activé </p>
{% else %}
<p class="alert"> Votre clef secrète n'est pas validé et donc non active </p>
{% endif %}
<form method="POST" action="{{ url_for('profil.set_totp') }}" >
<input type="disabled" class="hidden" name="shared_key" id="shared_key" value="{{ totp_shared_key }}"><br />
<fieldset role="group">
<input type="text" name="code_totp" id="code_totp" placeholder="Entrez votre code ici"><br />
<input type="submit" value="Valider ma clef secrète">
</fieldset>
</form>
{% if shared_key_validate %}
<p> Si vous voulez changer votre clef secrète vous devez d'abord supprimé votre clef actuelle</p>
<a href="{{ url_for('profil.del_totp') }}"> <button class="btn-alert"> Supprimer ma clef secrète </button></a>
{% endif %}
{% endblock %}

View File

@ -4,7 +4,7 @@ from markupsafe import escape
from flask_bcrypt import Bcrypt
from socket import gethostname
from os import remove, system
from tools.utils import email_disp, valid_token_register, valid_passwd, valid_username, gen_token
from tools.utils import email_disp, valid_token_register, valid_passwd, valid_username, gen_token, totp_is_valid
from tools.mailer import Mailer
app = Flask( 'pywallter' )
@ -19,8 +19,7 @@ DATAS_USER = app.config['DOSSIER_APP']
extensionimg = app.config['EXT_IMG']
DATABASE = app.config['DATABASE']
BASE_URL = app.config['BASE_URL']
BASE_URL = "http://"+app.config['HOST']+app.config['PORT']
SETUID = app.config['SETUID']
MAIL_SERVER = app.config['MAIL_SERVER']
XMPP_SERVER = app.config['XMPP_SERVER']
@ -39,17 +38,19 @@ def login() :
if request.method == 'POST' :
user = request.form['user']
password = request.form['passwd']
totp = request.form['code_totp']
conn = sqlite3.connect(DATABASE) # Connexion à la base de donnée
cursor = conn.cursor() # Création de l'objet "curseur"
cursor.execute("""SELECT name, passwd FROM users WHERE name=?""", (user,))
cursor.execute("""SELECT name, passwd, totp FROM users WHERE name=?""", (user,))
user_exist = cursor.fetchone()
conn.close()
if user_exist:
user = user_exist[0]
passwd_bcrypt = user_exist[1]
if user == request.form['user'] and bcrypt.check_password_hash(passwd_bcrypt, password) is True:
totp_key = user_exist[2]
if totp_is_valid(totp_key, totp) and user == request.form['user'] and bcrypt.check_password_hash(passwd_bcrypt, password) is True:
session['username'] = request.form['user']
resp = redirect(url_for('profil.profile', _external=True))
else:

View File

@ -8,7 +8,8 @@ import os
from shutil import copy
from socket import gethostname
from flask_bcrypt import Bcrypt
from tools.utils import email_disp, append_to_log, gen_token, valid_passwd, valid_token_register, get_user_by_token
from tools.utils import email_disp, append_to_log, gen_token, valid_passwd, valid_token_register, get_user_by_token, totp_is_valid
from pyotp import random_base32
profil = Blueprint('profil', __name__, template_folder='templates')
@ -28,7 +29,7 @@ DATAS_USER = app.config['DOSSIER_APP']
MAIL_SERVER = app.config['MAIL_SERVER']
XMPP_SERVER = app.config['XMPP_SERVER']
SETUID = app.config['SETUID']
BASE_URL = app.config['BASE_URL']
BASE_URL= "http://"+app.config['HOST']+":"+app.config['PORT']
BACKUP_TIME = app.config['BACKUP_TIME']
##################################################################################################
@ -135,34 +136,36 @@ def homepage():
@profil.route('/profil/change-password/', methods=['GET','POST'] )
def change_passwd() :
if 'username' in session:
UTILISATEUR='%s' % escape(session['username'])
user='%s' % escape(session['username'])
conn = sqlite3.connect(DATABASE) # Connexion à la base de donnée
cursor = conn.cursor() # Création de l'objet "curseur"
cursor.execute("""SELECT Mail, alias, xmpp FROM users WHERE name=?""", (UTILISATEUR,))
cursor.execute("""SELECT Mail, alias, xmpp, totp FROM users WHERE name=?""", (user,))
tmp = cursor.fetchone()
mailbox = dict()
mailbox['Mail'] = tmp[0]
mailbox['alias'] = tmp[1]
mailbox['xmpp'] = tmp[2]
shared_key_validate=True
account = dict()
account['Mail'] = tmp[0]
account['alias'] = tmp[1]
account['xmpp'] = tmp[2]
account['totp'] = tmp[3]
if request.method == 'POST' :
password = request.form['password']
password_confirm = request.form['passwd_confirm']
if password == password_confirm and valid_passwd(password):
if not(password == "") and password == password_confirm and valid_passwd(password):
mail_passwd_change = 0
xmpp_passwd_change = 0
passwd = request.form['password']
if MAIL_SERVER:
cmd = SETUID+ ' set_mail_passwd ' + '"'+mailbox['Mail']+'" '+ '"'+passwd+'"'
cmd = SETUID+ ' set_mail_passwd ' + '"'+account['Mail']+'" '+ '"'+passwd+'"'
mail_passwd_change = os.system(cmd)
if XMPP_SERVER:
tmp = mailbox['Mail'].split('@')
tmp = account['Mail'].split('@')
cmd = SETUID+ " prosodyctl register '"+tmp[0]+"' " + "'"+tmp[1]+"' " + "'"+passwd+"'"
xmpp_passwd_change = os.system(cmd)
if xmpp_passwd_change != 0:
@ -172,26 +175,35 @@ def change_passwd() :
if mail_passwd_change == 0:
passwd_bcrypt = bcrypt.generate_password_hash(passwd)
cursor.execute("UPDATE users SET passwd=? WHERE name=?",
(passwd_bcrypt, UTILISATEUR))
(passwd_bcrypt, user))
conn.commit()
TIME=time.strftime("%A %d %B %Y %H:%M:%S")
IP=request.environ['REMOTE_ADDR']
CLIENT_PLATFORM=request.headers.get('User-Agent')
log=TIME + ' - ' + IP + ' - ' + UTILISATEUR + ' - ' + CLIENT_PLATFORM + '\n' + '---> ' + "Changement du mot de passe" + '\n'
append_to_log(log, UTILISATEUR)
flash(u'Votre mot de passe a été changé', 'succes')
log=TIME + ' - ' + IP + ' - ' + user + ' - ' + CLIENT_PLATFORM + '\n' + '---> ' + "Changement du mot de passe" + '\n'
append_to_log(log, user)
flash(u'Votre mot de passe a été changé', 'success')
else:
if not( valid_passwd(password) ):
flash(u'Le mot de passe ne peut pas contenir les caractères " et &', 'error')
elif password == "":
flash(u' Vous ne pouvez pas ne pas mettre de mot de passe ou un mot de passe vide', 'error')
else:
flash(u'Les mot de passes ne sont pas identique :/ ', 'error')
flash(u'Les mot de passes ne sont pas identiques :/ ', 'error')
conn.close()
return render_template('mailbox.html',
if not(account['totp']):
account['totp'] = random_base32()
shared_key_validate = False
return render_template('mypassword.html',
section="Profil",
address=mailbox['Mail'],
alias=mailbox['alias'],
username=UTILISATEUR)
address=account['Mail'],
alias=account['alias'],
totp_shared_key=account['totp'],
shared_key_validate=shared_key_validate,
username=user)
else :
return redirect(BASE_URL, code=401)
@ -253,7 +265,8 @@ def change_passwd_lost(token) :
log=TIME + ' - ' + IP + ' - ' + user + ' - ' + CLIENT_PLATFORM + '\n' + '---> ' + "Changement du mot de passe" + '\n'
append_to_log(log, user)
flash(u'Votre mot de passe a été changé', 'succes')
cursor.execute("""UPDATE users set Lost_password_token='' where name=?""", (user,))
cursor.execute("""UPDATE users SET Lost_password_token='' where name=?""", (user,))
conn.commit()
conn.close()
resp = redirect(url_for('loginlogout.login'))
@ -273,6 +286,41 @@ def change_passwd_lost(token) :
return redirect(BASE_URL, code=401)
@profil.route('/set_totp/', methods=['POST'])
def set_totp():
if 'username' in session:
user='%s' % escape(session['username'])
conn = sqlite3.connect(DATABASE) # Connexion à la base de donnée
cursor = conn.cursor() # Création de l'objet "curseur"
shared_key = request.form['shared_key']
code_totp = request.form['code_totp']
if totp_is_valid(shared_key, code_totp) and code_totp !="" and shared_key != "":
print("shared_key: " +shared_key)
cursor.execute("""UPDATE users SET totp=? WHERE name=?""", (shared_key, user,))
conn.commit()
flash(u'Votre mot de passe à usage unique est configuré et actif.', 'success')
else:
flash(u'Le code de validation totp n\'est pas valide.', 'error')
conn.close()
return redirect(url_for('profil.change_passwd', _external=True))
else:
return redirect(BASE_URL, code=401)
@profil.route('/del_totp/', methods=['GET'])
def del_totp():
if 'username' in session:
user='%s' % escape(session['username'])
conn = sqlite3.connect(DATABASE) # Connexion à la base de donnée
cursor = conn.cursor() # Création de l'objet "curseur"
cursor.execute("""UPDATE users SET totp="" WHERE name=?""", (user,))
conn.commit()
conn.close()
return redirect(url_for('profil.change_passwd', _external=True))
@profil.route('/deltoken-password-lost/<token>', methods=['GET','POST'] )
def deltoken_passwd_lost(token) :
@ -415,6 +463,8 @@ def invitation():
else:
return redirect(BASE_URL, code=401)
@profil.route('/gen_token/', methods=['GET'])
def generate_token():
if 'username' in session: