Merge pull request #37 from allyssap/twofactor-authentication

Two factor authentication
This commit is contained in:
EmperorOfBananas 2023-03-14 19:47:58 -04:00 committed by GitHub
commit a633dba5ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 182 additions and 9 deletions

View File

@ -0,0 +1,59 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignoredPackages">
<value>
<list size="45">
<item index="0" class="java.lang.String" itemvalue="PyJWT" />
<item index="1" class="java.lang.String" itemvalue="alembic" />
<item index="2" class="java.lang.String" itemvalue="greenlet" />
<item index="3" class="java.lang.String" itemvalue="Babel" />
<item index="4" class="java.lang.String" itemvalue="python-dateutil" />
<item index="5" class="java.lang.String" itemvalue="SQLAlchemy" />
<item index="6" class="java.lang.String" itemvalue="python-dotenv" />
<item index="7" class="java.lang.String" itemvalue="MarkupSafe" />
<item index="8" class="java.lang.String" itemvalue="requests" />
<item index="9" class="java.lang.String" itemvalue="python-editor" />
<item index="10" class="java.lang.String" itemvalue="Jinja2" />
<item index="11" class="java.lang.String" itemvalue="Flask-Bootstrap" />
<item index="12" class="java.lang.String" itemvalue="Flask-Login" />
<item index="13" class="java.lang.String" itemvalue="redis" />
<item index="14" class="java.lang.String" itemvalue="dominate" />
<item index="15" class="java.lang.String" itemvalue="elasticsearch" />
<item index="16" class="java.lang.String" itemvalue="Pygments" />
<item index="17" class="java.lang.String" itemvalue="certifi" />
<item index="18" class="java.lang.String" itemvalue="urllib3" />
<item index="19" class="java.lang.String" itemvalue="itsdangerous" />
<item index="20" class="java.lang.String" itemvalue="Flask" />
<item index="21" class="java.lang.String" itemvalue="blinker" />
<item index="22" class="java.lang.String" itemvalue="email-validator" />
<item index="23" class="java.lang.String" itemvalue="dnspython" />
<item index="24" class="java.lang.String" itemvalue="six" />
<item index="25" class="java.lang.String" itemvalue="Werkzeug" />
<item index="26" class="java.lang.String" itemvalue="Flask-HTTPAuth" />
<item index="27" class="java.lang.String" itemvalue="langdetect" />
<item index="28" class="java.lang.String" itemvalue="Flask-WTF" />
<item index="29" class="java.lang.String" itemvalue="click" />
<item index="30" class="java.lang.String" itemvalue="Flask-SQLAlchemy" />
<item index="31" class="java.lang.String" itemvalue="chardet" />
<item index="32" class="java.lang.String" itemvalue="Flask-Moment" />
<item index="33" class="java.lang.String" itemvalue="WTForms" />
<item index="34" class="java.lang.String" itemvalue="PySocks" />
<item index="35" class="java.lang.String" itemvalue="httpie" />
<item index="36" class="java.lang.String" itemvalue="Flask-Mail" />
<item index="37" class="java.lang.String" itemvalue="Flask-Migrate" />
<item index="38" class="java.lang.String" itemvalue="pytz" />
<item index="39" class="java.lang.String" itemvalue="Mako" />
<item index="40" class="java.lang.String" itemvalue="visitor" />
<item index="41" class="java.lang.String" itemvalue="Flask-Babel" />
<item index="42" class="java.lang.String" itemvalue="idna" />
<item index="43" class="java.lang.String" itemvalue="requests-toolbelt" />
<item index="44" class="java.lang.String" itemvalue="rq" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

35
.idea/workspace.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="285438c8-50b9-448a-a339-55b76b90c8e5" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="HTML File" />
</list>
</option>
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultHtmlFileTemplate": "HTML File",
"node.js.selected.package.tslint": "(autodetect)"
}
}]]></component>
<component name="TaskManager">
<servers />
</component>
</project>

View File

@ -12,7 +12,6 @@ def verify_password(username, password):
if user and user.check_password(password):
return user
@basic_auth.error_handler
def basic_auth_error(status):
return error_response(status)

View File

@ -1,8 +1,31 @@
from flask import render_template, current_app
from flask_babel import _
from app.email import send_email
import math, random
def generate_otp(user):
# Declare a digits variable
# which stores all digits
digits = "0123456789"
otp = ""
# length of password can be changed
# by changing value in range
for i in range(4):
otp += digits[math.floor(random.random() * 10)]
user.otp = otp
return otp
def send_otp_email(user):
otp = generate_otp(user)
send_email(_('[Microblog] Your one time passcode'),
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('email/otp.txt',
user=user, otp=otp),
html_body=render_template('email/otp.html',
user=user, otp=otp))
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(_('[Microblog] Reset Your Password'),

View File

@ -4,13 +4,16 @@ 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()])
remember_me = BooleanField(_l('Remember Me'))
submit = SubmitField(_l('Sign In'))
class OTPForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
OTP = StringField(_l('OTP'), validators=[DataRequired()]) ###EqualTo(otp)
submit = SubmitField(_l('Log in') )
class RegistrationForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])

View File

@ -5,15 +5,12 @@ from flask_babel import _
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, \
ResetPasswordRequestForm, ResetPasswordForm
ResetPasswordRequestForm, ResetPasswordForm, OTPForm
from app.models import User
from app.auth.email import send_password_reset_email
@bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
@ -23,10 +20,26 @@ def login():
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
next_page = url_for('auth.otp_login')
return redirect(next_page)
return render_template('auth/login.html', title=_('Sign In'), form=form)
@bp.route('/otp', methods=['GET', 'POST'])
def otp_login():
form = OTPForm()
user = User.query.filter_by(username=form.username.data).first()
otp = form.OTP.data
if user:
send_otp_email(user)
flash(_('Check your email for your OTP'))
return redirect(url_for('auth.otp_login'))
if otp != user.otp:
flash(_('Invalid OTP'))
return redirect(url_for('auth.otp_login'))
if form.validate_on_submit():
return redirect(url_for('main.index'))
return render_template('auth/otp_login.html', title=_('Enter OTP'),
form=form)
@bp.route('/logout')
def logout():

View File

@ -26,7 +26,6 @@ class EditProfileForm(FlaskForm):
class EmptyForm(FlaskForm):
submit = SubmitField('Submit')
class PostForm(FlaskForm):
post = TextAreaField(_l('Say something'), validators=[DataRequired()])
submit = SubmitField(_l('Submit'))

View File

@ -88,7 +88,6 @@ followers = db.Table(
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)
class User(UserMixin, PaginatedAPIMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
@ -96,6 +95,7 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
about_me = db.Column(db.String(140))
otp = db.Column(db.String(4))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
token = db.Column(db.String(32), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
@ -118,6 +118,13 @@ class User(UserMixin, PaginatedAPIMixin, db.Model):
def __repr__(self):
return '<User {}>'.format(self.username)
##
## def set_otp(self, otp):
## self.otp = otp
## def get_otp(self):
## return self.otp
def set_password(self, password):
self.password_hash = generate_password_hash(password)

BIN
app/templates/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>{{ _('Validation') }}</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
<br>
<p>{{ _('New User?') }} <a href="{{ url_for('auth.register') }}">{{ _('Click to Register!') }}</a></p>
<p>{{ _('Returning User?') }} <a href="{{ url_for('auth.login') }}">{{ _('Click to Login!') }}</a></p>
{% endblock %}

View File

@ -0,0 +1,9 @@
<p>Dear {{ user.username }},</p>
<p>
Here is your one time passcode
<p>{{otp}}</p>
</p>
<p>If you have not requested an otp, simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Microblog Team</p>

View File

@ -0,0 +1,11 @@
Dear {{ user.username }},
Here is your one time passcode:
{{otp}}
If you have not requested a otp, simply ignore this message.
Sincerely,
The Microblog Team

View File

@ -23,6 +23,7 @@ def upgrade():
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('email', sa.String(length=120), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('otp', sa.Integer()),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)