Merge pull request #37 from allyssap/twofactor-authentication
Two factor authentication
This commit is contained in:
commit
a633dba5ac
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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()])
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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 %}
|
|
@ -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>
|
|
@ -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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue