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