forked from Github-Mirrors/canaille
adds mail sending to admin if failure of api HIBP request to check if password is compromised
This commit is contained in:
parent
093397256b
commit
ca7f718353
6 changed files with 161 additions and 11 deletions
|
@ -16,6 +16,7 @@ from canaille.app.i18n import gettext as _
|
|||
from canaille.app.i18n import locale_selector
|
||||
from canaille.app.i18n import timezone_selector
|
||||
from canaille.backends import Backend
|
||||
from canaille.core.mails import send_comprimised_password_check_failure_mail
|
||||
|
||||
from . import validate_uri
|
||||
from .flask import request_is_htmx
|
||||
|
@ -84,7 +85,7 @@ def password_strength_calculator(password):
|
|||
return strength_score
|
||||
|
||||
|
||||
def pwned_password_validator(form, field):
|
||||
def compromised_password_validator(form, field):
|
||||
try:
|
||||
from hashlib import sha1
|
||||
|
||||
|
@ -94,18 +95,32 @@ def pwned_password_validator(form, field):
|
|||
|
||||
hashed_password = sha1(field.data.encode("utf-8")).hexdigest()
|
||||
hashed_password_splited = (hashed_password[:5].upper(), hashed_password[5:].upper())
|
||||
|
||||
api_url = f"https://api.pwnedpasswords.com/range/{hashed_password_splited[0]}"
|
||||
|
||||
try:
|
||||
response = requests.api.get(
|
||||
f"https://api.pwnedpasswords.com/range/{hashed_password_splited[0]}",
|
||||
timeout=10,
|
||||
)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
response = requests.api.get(api_url, timeout=10)
|
||||
except Exception as e:
|
||||
print("Error: " + str(e))
|
||||
|
||||
if current_app.features.has_smtp and not request_is_htmx():
|
||||
if form.user is not None:
|
||||
user_name = form.user.user_name
|
||||
user_email = form.user.emails[0]
|
||||
else:
|
||||
user_name = form["user_name"].data
|
||||
user_email = form["emails"].data[0]
|
||||
|
||||
send_comprimised_password_check_failure_mail(
|
||||
api_url, user_name, user_email, hashed_password
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
decoded_response = response.content.decode("utf8").split("\r\n")
|
||||
|
||||
for each in decoded_response:
|
||||
if hashed_password_splited[1] == each.split(":")[0]:
|
||||
if hashed_password_splited[1] in each.split(":")[0]:
|
||||
raise wtforms.ValidationError(_("This password is compromised."))
|
||||
|
||||
|
||||
|
|
|
@ -34,10 +34,10 @@ from canaille.app.flask import smtp_needed
|
|||
from canaille.app.flask import user_needed
|
||||
from canaille.app.forms import IDToModel
|
||||
from canaille.app.forms import TableForm
|
||||
from canaille.app.forms import compromised_password_validator
|
||||
from canaille.app.forms import is_readonly
|
||||
from canaille.app.forms import password_length_validator
|
||||
from canaille.app.forms import password_too_long_validator
|
||||
from canaille.app.forms import pwned_password_validator
|
||||
from canaille.app.forms import set_readonly
|
||||
from canaille.app.forms import set_writable
|
||||
from canaille.app.i18n import gettext as _
|
||||
|
@ -317,7 +317,7 @@ def registration(data=None, hash=None):
|
|||
wtforms.validators.DataRequired(),
|
||||
password_length_validator,
|
||||
password_too_long_validator,
|
||||
pwned_password_validator,
|
||||
compromised_password_validator,
|
||||
]
|
||||
form["password2"].validators = [
|
||||
wtforms.validators.DataRequired(),
|
||||
|
|
|
@ -10,12 +10,12 @@ from canaille.app.forms import BaseForm
|
|||
from canaille.app.forms import DateTimeUTCField
|
||||
from canaille.app.forms import Form
|
||||
from canaille.app.forms import IDToModel
|
||||
from canaille.app.forms import compromised_password_validator
|
||||
from canaille.app.forms import email_validator
|
||||
from canaille.app.forms import is_uri
|
||||
from canaille.app.forms import password_length_validator
|
||||
from canaille.app.forms import password_too_long_validator
|
||||
from canaille.app.forms import phone_number
|
||||
from canaille.app.forms import pwned_password_validator
|
||||
from canaille.app.forms import set_readonly
|
||||
from canaille.app.forms import unique_values
|
||||
from canaille.app.i18n import gettext
|
||||
|
@ -266,7 +266,7 @@ PROFILE_FORM_FIELDS = dict(
|
|||
wtforms.validators.Optional(),
|
||||
password_length_validator,
|
||||
password_too_long_validator,
|
||||
pwned_password_validator,
|
||||
compromised_password_validator,
|
||||
],
|
||||
render_kw={
|
||||
"autocomplete": "new-password",
|
||||
|
|
|
@ -210,3 +210,43 @@ def send_registration_mail(email, registration_url):
|
|||
html=html_body,
|
||||
attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None,
|
||||
)
|
||||
|
||||
|
||||
def send_comprimised_password_check_failure_mail(
|
||||
check_password_url, user_name, user_email, hashed_password
|
||||
):
|
||||
base_url = url_for("core.account.index", _external=True)
|
||||
logo_cid, logo_filename, logo_raw = logo()
|
||||
|
||||
subject = _("Pwned password check incident on {website_name}").format(
|
||||
website_name=current_app.config["CANAILLE"]["NAME"]
|
||||
)
|
||||
text_body = render_template(
|
||||
"mails/pwned-password-non-checked.txt",
|
||||
site_name=current_app.config["CANAILLE"]["NAME"],
|
||||
site_url=base_url,
|
||||
check_password_url=check_password_url,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
html_body = render_template(
|
||||
"mails/pwned-password-non-checked.html",
|
||||
site_name=current_app.config["CANAILLE"]["NAME"],
|
||||
site_url=base_url,
|
||||
check_password_url=check_password_url,
|
||||
logo=f"cid:{logo_cid[1:-1]}" if logo_cid else None,
|
||||
title=subject,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
|
||||
return send_email(
|
||||
subject=subject,
|
||||
# line to change with admin group mails....
|
||||
recipient="sebastien@yaal.coop",
|
||||
text=text_body,
|
||||
html=html_body,
|
||||
attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="https://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<style type="text/css" style="font-weight: 300">@import url({{ url_for('static', filename='fonts/lato.css', _external=True) }});</style>
|
||||
<title>{{ title }}</title>
|
||||
</head>
|
||||
<body style="color: rgba(0,0,0,.87); padding: 1em; margin: auto; width: 700px; font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; font-weight: 400; font-size: 14px;">
|
||||
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="font-weight: 400; background: #fff; font-size: 1rem; margin-top: 0; margin-bottom: 0; width: 700px;">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<h3 style="font-weight: 700; line-height: 1.3em; font-size: 1.3rem; padding: .8rem 1rem; margin: 0; box-shadow: none; border: 1px solid #d4d4d5; border-radius: .3rem .3rem 0 0;">
|
||||
{% if logo %}
|
||||
<img src="{{ logo }}" alt="{{ site_name }}" style="font-size: 1.3rem; border-style: none; width: 50px; display: inline-block; margin-top: .14em; vertical-align: middle;">
|
||||
{% endif %}
|
||||
<div style="font-size: 1.3rem; display: inline-block; padding-left: .75rem; vertical-align: middle;">
|
||||
{% trans %}Verification failed if password is compromised.{% endtrans %}
|
||||
</div>
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}Our services were unable to verify if the {{ user_name }}'s password is compromised.{% endtrans %}</br>
|
||||
</p>
|
||||
<p>
|
||||
{% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %}</br>
|
||||
{% trans %}Follow this steps : {% endtrans %}</br>
|
||||
{% trans %}1. click on the link above "Check if password is pwned"{% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td colspan="2" style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="{{ check_password_url }}" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %}Check if password is compromised{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %}</br>
|
||||
{% trans %}3. if the password is in the list :{% endtrans %}</br>
|
||||
{% trans %}3.1. open this link and reset user's password. {% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td colspan="2" style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="http://127.0.0.1:5000/profile/{{ user_name }}/settings" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %} Reset {{ user_name }}'s password {% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4em; font-size: 1em; margin: 0; border-radius: 0; text-align: justify; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5;">
|
||||
<p>
|
||||
{% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}{% endtrans %}</br>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr style="margin: 0; border-radius: 0; text-align:center; border-left: 1px solid #d4d4d5; border-right: 1px solid #d4d4d5; border-bottom: 1px solid #d4d4d5;">
|
||||
<td style="background: #e0e1e2; width:50%; border-radius: 0 0 0 .3rem;">
|
||||
<a href="{{ site_url }}" style="width: 100%; display: inline-block; vertical-align: middle; color: rgba(0,0,0,.6); padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem;margin: 0; border-left: none; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{{ site_name }}</a>
|
||||
</td>
|
||||
<td style="background: #2185d0; width:50%; border-radius: 0 0 .3rem 0;">
|
||||
<a href="{{ check_password_url }}" style="width: 100%; display: inline-block; vertical-align: middle; padding: .8em 0; font-weight: 700; line-height: 1em; text-align: center; text-decoration: none; font-size: 1rem; color: #fff; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %}Check if password is compromised{% endtrans %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
14
canaille/core/templates/mails/pwned-password-non-checked.txt
Normal file
14
canaille/core/templates/mails/pwned-password-non-checked.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
# {% trans %}Verification failed if password is compromised.{% endtrans %}
|
||||
|
||||
{% trans %}Our services were unable to verify if the http://127.0.0.1:5000/profile/{{ user_name }}/settings's password is compromised.{% endtrans %}
|
||||
|
||||
{% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %}
|
||||
{% trans %}Follow this steps : {% endtrans %}
|
||||
{% trans %}1. click on the link above "Check if password is pwned"{% endtrans %}
|
||||
{% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %}
|
||||
{% trans %}3. if the password is in the list :{% endtrans %}
|
||||
{% trans %}3.1. open this link http://127.0.0.1:5000/profile/{{ user_name }}/settings and reset user's password.{% endtrans %}
|
||||
{% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}.{% endtrans %}
|
||||
|
||||
{% trans %}Check if password is compromised{% endtrans %}: {{ check_password_url }}
|
||||
{{ site_name }}: {{ site_url }}
|
Loading…
Reference in a new issue