mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
Fix email confirmations and password resets (#795)
* Fix email confirmations and improve test. * Breaks confirm.html page because of change from team to user * Clean up admin mail settings to use new label/small structure * Fix password resets from double hashing passwords
This commit is contained in:
34
CTFd/auth.py
34
CTFd/auth.py
@@ -7,8 +7,6 @@ from flask import (
|
|||||||
session,
|
session,
|
||||||
Blueprint,
|
Blueprint,
|
||||||
)
|
)
|
||||||
from passlib.hash import bcrypt_sha256
|
|
||||||
|
|
||||||
from CTFd.models import db, Users, Teams
|
from CTFd.models import db, Users, Teams
|
||||||
|
|
||||||
from CTFd.utils import get_config, get_app_config
|
from CTFd.utils import get_config, get_app_config
|
||||||
@@ -17,6 +15,7 @@ from CTFd.utils import user as current_user
|
|||||||
from CTFd.utils import config, validators
|
from CTFd.utils import config, validators
|
||||||
from CTFd.utils import email
|
from CTFd.utils import email
|
||||||
from CTFd.utils.security.auth import login_user, logout_user
|
from CTFd.utils.security.auth import login_user, logout_user
|
||||||
|
from CTFd.utils.security.passwords import hash_password, check_password
|
||||||
from CTFd.utils.logging import log
|
from CTFd.utils.logging import log
|
||||||
from CTFd.utils.decorators.visibility import check_registration_visibility
|
from CTFd.utils.decorators.visibility import check_registration_visibility
|
||||||
from CTFd.utils.modes import TEAMS_MODE, USERS_MODE
|
from CTFd.utils.modes import TEAMS_MODE, USERS_MODE
|
||||||
@@ -46,8 +45,8 @@ def confirm(data=None):
|
|||||||
except (BadSignature, TypeError, base64.binascii.Error):
|
except (BadSignature, TypeError, base64.binascii.Error):
|
||||||
return render_template('confirm.html', errors=['Your confirmation token is invalid'])
|
return render_template('confirm.html', errors=['Your confirmation token is invalid'])
|
||||||
|
|
||||||
team = Users.query.filter_by(email=user_email).first_or_404()
|
user = Users.query.filter_by(email=user_email).first_or_404()
|
||||||
team.verified = True
|
user.verified = True
|
||||||
log('registrations', format="[{date}] {ip} - successful password reset for {name}")
|
log('registrations', format="[{date}] {ip} - successful password reset for {name}")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
@@ -59,24 +58,19 @@ def confirm(data=None):
|
|||||||
if not current_user.authed():
|
if not current_user.authed():
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
team = Users.query.filter_by(id=session['id']).first_or_404()
|
user = Users.query.filter_by(id=session['id']).first_or_404()
|
||||||
|
if user.verified:
|
||||||
|
return redirect(url_for('views.settings'))
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# User wants to resend their confirmation email
|
# User wants to resend their confirmation email
|
||||||
if team.verified:
|
email.verify_email_address(user.email)
|
||||||
return redirect(url_for('views.profile'))
|
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
|
||||||
else:
|
return render_template('confirm.html', user=user, infos=['Your confirmation email has been resent!'])
|
||||||
email.verify_email_address(team.email)
|
|
||||||
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
|
|
||||||
return render_template('confirm.html', team=team, infos=['Your confirmation email has been resent!'])
|
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
# User has been directed to the confirm page
|
# User has been directed to the confirm page
|
||||||
team = Users.query.filter_by(id=session['id']).first_or_404()
|
return render_template('confirm.html', user=user)
|
||||||
if team.verified:
|
|
||||||
# If user is already verified, redirect to their profile
|
|
||||||
return redirect(url_for('views.profile'))
|
|
||||||
return render_template('confirm.html', team=team)
|
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/reset_password', methods=['POST', 'GET'])
|
@auth.route('/reset_password', methods=['POST', 'GET'])
|
||||||
@@ -94,10 +88,10 @@ def reset_password(data=None):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template('reset_password.html', mode='set')
|
return render_template('reset_password.html', mode='set')
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
team = Users.query.filter_by(name=name).first_or_404()
|
user = Users.query.filter_by(name=name).first_or_404()
|
||||||
team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
|
user.password = request.form['password'].strip()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log('logins', format="[{date}] {ip} - successful password reset for {name}")
|
log('logins', format="[{date}] {ip} - successful password reset for {name}", name=name)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
@@ -226,7 +220,7 @@ def login():
|
|||||||
user = Users.query.filter_by(name=name).first()
|
user = Users.query.filter_by(name=name).first()
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
if user and bcrypt_sha256.verify(request.form['password'], user.password):
|
if user and check_password(request.form['password'], user.password):
|
||||||
session.regenerate()
|
session.regenerate()
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
|||||||
@@ -11,26 +11,37 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mail From Address:</label>
|
<label>
|
||||||
<input class="form-control" id='mailfrom_addr' name='mailfrom_addr' type='text'
|
Mail From Address<br>
|
||||||
placeholder="Email address used to send email"
|
<small class="form-text text-muted">
|
||||||
{% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
|
Email address used to send email
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
<input class="form-control" id='mailfrom_addr' name='mailfrom_addr' type='text' {% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="mailserver">
|
<div role="tabpanel" class="tab-pane active" id="mailserver">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mail Server Address:</label>
|
<label>
|
||||||
|
Mail Server Address<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mail Server Address
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
<input class="form-control" id='mail_server' name='mail_server' type='text'
|
<input class="form-control" id='mail_server' name='mail_server' type='text'
|
||||||
placeholder="Mail Server Address"
|
|
||||||
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
|
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mail Server Port:</label>
|
<label>
|
||||||
|
Mail Server Port<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mail Server Port
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
<input class="form-control" id='mail_port' name='mail_port' type='text'
|
<input class="form-control" id='mail_port' name='mail_port' type='text'
|
||||||
placeholder="Mail Server Port"
|
|
||||||
{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
|
{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -42,20 +53,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="mail_username_password">
|
<div id="mail_username_password">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Username:</label>
|
<label>
|
||||||
|
Username<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mail Server Account Username
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
{% if mail_username is defined and mail_username != None %}
|
{% if mail_username is defined and mail_username != None %}
|
||||||
<label for="mail_username"><sup>A mail server username is currently set</sup></label>
|
<label>
|
||||||
|
<sup class="form-text text-muted">A mail server username is currently set</sup>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input class="form-control" id='mail_username' name='mail_username' autocomplete='off' type='text'
|
<input class="form-control" id='mail_username' name='mail_username' autocomplete='off' type='text'>
|
||||||
placeholder="Username">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="mail_password">Password:</label>
|
<label for="mail_password">
|
||||||
|
Password<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mail Server Account Password
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
{% if mail_password is defined and mail_password != None %}
|
{% if mail_password is defined and mail_password != None %}
|
||||||
<label for="mail_p"><sup>An mail server password is currently set</sup></label>
|
<label>
|
||||||
|
<sup class="form-text text-muted">An mail server password is currently set</sup>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input class="form-control" id='mail_password' name='mail_password' autocomplete='off' type='password'
|
<input class="form-control" id='mail_password' name='mail_password' autocomplete='off' type='password'>
|
||||||
placeholder="Password">
|
|
||||||
</div>
|
</div>
|
||||||
<sup>Uncheck setting and update to remove username and password</sup>
|
<sup>Uncheck setting and update to remove username and password</sup>
|
||||||
<br>
|
<br>
|
||||||
@@ -76,15 +99,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="mailgun">
|
<div role="tabpanel" class="tab-pane" id="mailgun">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mailgun API Base URL:</label>
|
<label>
|
||||||
|
Mailgun API Base URL<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mailgun API Base URL
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
<input class="form-control" id='mailgun_base_url' name='mailgun_base_url' type='text'
|
<input class="form-control" id='mailgun_base_url' name='mailgun_base_url' type='text'
|
||||||
placeholder="Mailgun API Base URL"
|
|
||||||
{% if mailgun_base_url is defined and mailgun_base_url != None %}value="{{ mailgun_base_url }}"{% endif %}>
|
{% if mailgun_base_url is defined and mailgun_base_url != None %}value="{{ mailgun_base_url }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Mailgun API Key:</label>
|
<label>
|
||||||
|
Mailgun API Key<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Mailgun API Key
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
<input class="form-control" id='mailgun_api_key' name='mailgun_api_key' type='text'
|
<input class="form-control" id='mailgun_api_key' name='mailgun_api_key' type='text'
|
||||||
placeholder="Mailgun API Key"
|
|
||||||
{% if mailgun_api_key is defined and mailgun_api_key != None %}value="{{ mailgun_api_key }}"{% endif %}>
|
{% if mailgun_api_key is defined and mailgun_api_key != None %}value="{{ mailgun_api_key }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
aria-hidden="true">×</span></button>
|
aria-hidden="true">×</span></button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if team %}
|
{% if user %}
|
||||||
<h3 class="text-center">
|
<h3 class="text-center">
|
||||||
We've sent a confirmation email to {{ team.email }}
|
We've sent a confirmation email to {{ user.email }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -40,7 +40,9 @@
|
|||||||
</h4>
|
</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if username %}
|
<hr>
|
||||||
|
|
||||||
|
{% if name %}
|
||||||
<form method="POST" action="{{ url_for('auth.confirm') }}">
|
<form method="POST" action="{{ url_for('auth.confirm') }}">
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
Need to resend the confirmation email?
|
Need to resend the confirmation email?
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from CTFd.models import Users
|
from CTFd.models import Users
|
||||||
from CTFd.utils import set_config, get_config
|
from CTFd.utils import set_config, get_config
|
||||||
from CTFd.utils.security.signing import serialize
|
from CTFd.utils.security.signing import serialize
|
||||||
|
from CTFd.utils.security.passwords import hash_password, check_password
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
from tests.helpers import *
|
from tests.helpers import *
|
||||||
from mock import patch
|
from mock import patch
|
||||||
@@ -133,11 +134,10 @@ def test_user_isnt_admin():
|
|||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2019-02-24 03:21:34")
|
|
||||||
def test_expired_confirmation_links():
|
def test_expired_confirmation_links():
|
||||||
"""Test that expired confirmation links are reported to the user"""
|
"""Test that expired confirmation links are reported to the user"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context(), freeze_time("2019-02-24 03:21:34"):
|
||||||
set_config('verify_emails', True)
|
set_config('verify_emails', True)
|
||||||
|
|
||||||
register_user(app, email="user@user.com")
|
register_user(app, email="user@user.com")
|
||||||
@@ -172,7 +172,6 @@ def test_invalid_confirmation_links():
|
|||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2019-02-24 03:21:34")
|
|
||||||
def test_expired_reset_password_link():
|
def test_expired_reset_password_link():
|
||||||
"""Test that expired reset password links are reported to the user"""
|
"""Test that expired reset password links are reported to the user"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
@@ -185,7 +184,7 @@ def test_expired_reset_password_link():
|
|||||||
|
|
||||||
register_user(app, name="user1", email="user@user.com")
|
register_user(app, name="user1", email="user@user.com")
|
||||||
|
|
||||||
with app.test_client() as client:
|
with app.test_client() as client, freeze_time("2019-02-24 03:21:34"):
|
||||||
# user@user.com "2012-01-14 03:21:34"
|
# user@user.com "2012-01-14 03:21:34"
|
||||||
forgot_link = 'http://localhost/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.cAGwAy8cK1T0saEEbrDEBVF2plI'
|
forgot_link = 'http://localhost/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.cAGwAy8cK1T0saEEbrDEBVF2plI'
|
||||||
r = client.get(forgot_link)
|
r = client.get(forgot_link)
|
||||||
@@ -230,11 +229,10 @@ def test_contact_for_password_reset():
|
|||||||
|
|
||||||
|
|
||||||
@patch('smtplib.SMTP')
|
@patch('smtplib.SMTP')
|
||||||
@freeze_time("2012-01-14 03:21:34")
|
|
||||||
def test_user_can_confirm_email(mock_smtp):
|
def test_user_can_confirm_email(mock_smtp):
|
||||||
"""Test that a user is capable of confirming their email address"""
|
"""Test that a user is capable of confirming their email address"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context(), freeze_time("2012-01-14 03:21:34"):
|
||||||
# Set CTFd to only allow confirmed users and send emails
|
# Set CTFd to only allow confirmed users and send emails
|
||||||
set_config('verify_emails', True)
|
set_config('verify_emails', True)
|
||||||
set_config('mail_server', 'localhost')
|
set_config('mail_server', 'localhost')
|
||||||
@@ -251,6 +249,9 @@ def test_user_can_confirm_email(mock_smtp):
|
|||||||
|
|
||||||
client = login_as_user(app, name="user1", password="password")
|
client = login_as_user(app, name="user1", password="password")
|
||||||
|
|
||||||
|
r = client.get('http://localhost/confirm')
|
||||||
|
assert "Need to resend the confirmation email?" in r.get_data(as_text=True)
|
||||||
|
|
||||||
# smtp.sendmail was called
|
# smtp.sendmail was called
|
||||||
mock_smtp.return_value.sendmail.assert_called()
|
mock_smtp.return_value.sendmail.assert_called()
|
||||||
|
|
||||||
@@ -258,6 +259,9 @@ def test_user_can_confirm_email(mock_smtp):
|
|||||||
data = {
|
data = {
|
||||||
"nonce": sess.get('nonce')
|
"nonce": sess.get('nonce')
|
||||||
}
|
}
|
||||||
|
r = client.post('http://localhost/confirm', data=data)
|
||||||
|
assert "confirmation email has been resent" in r.get_data(as_text=True)
|
||||||
|
|
||||||
r = client.get('/challenges')
|
r = client.get('/challenges')
|
||||||
assert r.location == "http://localhost/confirm" # We got redirected to /confirm
|
assert r.location == "http://localhost/confirm" # We got redirected to /confirm
|
||||||
|
|
||||||
@@ -269,16 +273,18 @@ def test_user_can_confirm_email(mock_smtp):
|
|||||||
# The team is now verified
|
# The team is now verified
|
||||||
user = Users.query.filter_by(email='user@user.com').first()
|
user = Users.query.filter_by(email='user@user.com').first()
|
||||||
assert user.verified is True
|
assert user.verified is True
|
||||||
|
|
||||||
|
r = client.get('http://localhost/confirm')
|
||||||
|
assert r.location == "http://localhost/settings"
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
@patch('smtplib.SMTP')
|
@patch('smtplib.SMTP')
|
||||||
@freeze_time("2012-01-14 03:21:34")
|
|
||||||
def test_user_can_reset_password(mock_smtp):
|
def test_user_can_reset_password(mock_smtp):
|
||||||
"""Test that a user is capable of resetting their password"""
|
"""Test that a user is capable of resetting their password"""
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context(), freeze_time("2012-01-14 03:21:34"):
|
||||||
# Set CTFd to send emails
|
# Set CTFd to send emails
|
||||||
set_config('mail_server', 'localhost')
|
set_config('mail_server', 'localhost')
|
||||||
set_config('mail_port', 25)
|
set_config('mail_port', 25)
|
||||||
@@ -333,7 +339,7 @@ def test_user_can_reset_password(mock_smtp):
|
|||||||
|
|
||||||
# Make sure that the user's password changed
|
# Make sure that the user's password changed
|
||||||
user = Users.query.filter_by(email="user@user.com").first()
|
user = Users.query.filter_by(email="user@user.com").first()
|
||||||
assert user.password != user_password_saved
|
assert check_password('passwordtwo', user.password)
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user