This commit is contained in:
manrajg13 2023-03-06 14:11:02 -05:00
parent 6f47fdd65b
commit 7bbd883955
11 changed files with 207 additions and 5 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
TWILIO_ACCOUNT_SID='AC0bd1e642a7054c751da1cce44df0b627'
TWILIO_AUTH_TOKEN='50f07accf2e7e5888a28f2657957ddab'
TWILIO_VERIFY_SERVICE_ID='VAb34d1b5d153b89ef0706728107612a4e'

View File

@ -4,7 +4,6 @@ from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from flask_babel import _, lazy_gettext as _l
from app.models import User
class LoginForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
password = PasswordField(_l('Password'), validators=[DataRequired()])
@ -12,6 +11,26 @@ class LoginForm(FlaskForm):
submit = SubmitField(_l('Sign In'))
class Confirm2faForm(FlaskForm):
token = StringField('Token')
submit = SubmitField('Verify')
class Enable2faForm(FlaskForm):
verification_phone = StringField('Phone', validators=[DataRequired()])
submit = SubmitField('Enable 2FA')
def validate_verification_phone(self, verification_phone):
try:
return
except:
print("An exception occurred")
class Disable2faForm(FlaskForm):
submit = SubmitField('Disable 2FA')
class RegistrationForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
email = StringField(_l('Email'), validators=[DataRequired(), Email()])

View File

@ -1,10 +1,11 @@
from flask import render_template, redirect, url_for, flash, request
from app.auth.twilio_verify import check_verification_token, request_verification_token
from flask import render_template, redirect, url_for, flash, request, session
from werkzeug.urls import url_parse
from flask_login import login_user, logout_user, current_user
from flask_login import login_user, login_required, logout_user, current_user
from flask_babel import _
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, \
from app.auth.forms import Confirm2faForm, Disable2faForm, Enable2faForm, LoginForm, RegistrationForm, \
ResetPasswordRequestForm, ResetPasswordForm
from app.models import User
from app.auth.email import send_password_reset_email
@ -20,14 +21,68 @@ def login():
if user is None or not user.check_password(form.password.data):
flash(_('Invalid username or password'))
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if user.two_factor_enabled():
request_verification_token(user.verification_phone)
session['username'] = user.username
session['phone'] = user.verification_phone
return redirect(url_for(
'auth.verify_2fa', next=next_page,
remember='1' if form.remember_me.data else '0'))
login_user(user, remember=form.remember_me.data)
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title=_('Sign In'), form=form)
@bp.route('/verify2fa', methods=['GET', 'POST'])
def verify_2fa():
form = Confirm2faForm()
if form.validate_on_submit():
phone = session['phone']
if check_verification_token(phone, form.token.data):
del session['phone']
if current_user.is_authenticated:
current_user.verification_phone = phone
db.session.commit()
flash('Two-factor authentication is now enabled')
return redirect(url_for('main.index'))
else:
username = session['username']
del session['username']
user = User.query.filter_by(username=username).first()
next_page = request.args.get('next')
remember = request.args.get('remember', '0') == '1'
login_user(user, remember=remember)
return redirect(next_page)
form.token.errors.append('Invalid token')
return render_template('auth/verify_2fa.html', form=form)
@bp.route('/enable_2fa', methods=['GET', 'POST'])
@login_required
def enable_2fa():
form = Enable2faForm()
if form.validate_on_submit():
session['phone'] = form.verification_phone.data
request_verification_token(session['phone'])
return redirect(url_for('auth.verify_2fa'))
return render_template('auth/enable_2fa.html', form=form)
@bp.route('/disable_2fa', methods=['GET', 'POST'])
@login_required
def disable_2fa():
form = Disable2faForm()
if form.validate_on_submit():
current_user.verification_phone = None
db.session.commit()
flash('Two-factor authentication is now disabled.')
return redirect(url_for('main.index'))
return render_template('auth/disable_2fa.html', form=form)
@bp.route('/logout')
def logout():
logout_user()

26
app/auth/twilio_verify.py Normal file
View File

@ -0,0 +1,26 @@
from flask import current_app
from twilio.rest import Client, TwilioException
def _get_twilio_verify_client():
return Client(
current_app.config['TWILIO_ACCOUNT_SID'],
current_app.config['TWILIO_AUTH_TOKEN']).verify.services(
current_app.config['TWILIO_VERIFY_SERVICE_ID'])
def request_verification_token(phone):
verify = _get_twilio_verify_client()
try:
verify.verifications.create(to=phone, channel='sms')
except TwilioException:
verify.verifications.create(to=phone, channel='call')
def check_verification_token(phone, token):
verify = _get_twilio_verify_client()
try:
result = verify.verification_checks.create(to=phone, code=token)
except TwilioException:
return False
return result.status == 'approved'

View File

@ -94,6 +94,7 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
verification_phone = db.Column(db.String(16))
posts = db.relationship('Post', backref='author', lazy='dynamic')
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
@ -123,6 +124,9 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def two_factor_enabled(self):
return self.verification_phone is not None
def avatar(self, size):
digest = md5(self.email.lower().encode('utf-8')).hexdigest()

View File

@ -0,0 +1,13 @@
{# app/templates/auth/disable_2fa.html #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Disable Two-Factor Authentication</h1>
<p>Please click the button below to disable two-factor authentication on your account.</p>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Enable Two-Factor Authentication</h1>
<p>Please enter your mobile number to activate two-factor authentication on your account.</p>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/16.0.4/css/intlTelInput.css">
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/16.0.4/js/intlTelInput-jquery.min.js"></script>
<script>
$("#verification_phone").css({position: 'absolute', top: '-9999px', left: '-9999px'});
$("#verification_phone").parent().append('<div><input type="tel" id="_verification_phone"></div>');
$("#_verification_phone").intlTelInput({
separateDialCode: true,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/16.0.4/js/utils.js",
});
$("#_verification_phone").intlTelInput("setNumber", $('#verification_phone').val());
$('#_verification_phone').blur(function() {
$('#verification_phone').val($('#_verification_phone').intlTelInput("getNumber"));
});
</script>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Two-Factor Authentication</h1>
<p>Please enter the token that was sent to your phone.</p>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -34,6 +34,11 @@
{% if user != current_user %}
<p><a href="{{ url_for('main.send_message', recipient=user.username) }}">{{ _('Send private message') }}</a></p>
{% endif %}
{% if not user.two_factor_enabled() %}
<p><a href="{{ url_for('auth.enable_2fa') }}">{{ _('Enable two-factor authentication') }}</a></p>
{% else %}
<p><a href="{{ url_for('auth.disable_2fa') }}">{{ _('Disable two-factor authentication') }}</a></p>
{% endif %}
</td>
</tr>
</table>

View File

@ -11,6 +11,9 @@ class Config(object):
'postgres://', 'postgresql://') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID')
TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_ACCOUNT_TOKEN')
TWILIO_VERIFY_SERVICE_ID = os.environ.get('TWILIO_VERIFY_SERVICE_ID')
LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)

View File

@ -0,0 +1,28 @@
"""two-factor authentication
Revision ID: 23e9d8ca48c2
Revises: 834b1a697901
Create Date: 2023-03-04 14:26:06.269415
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '23e9d8ca48c2'
down_revision = '834b1a697901'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('verification_phone', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'verification_phone')
# ### end Alembic commands ###