From c0a13afe13cc990b96fade728a55c2cff2e881da Mon Sep 17 00:00:00 2001 From: Allyssa Poulin <40319407+allyssap@users.noreply.github.com> Date: Sun, 12 Mar 2023 17:35:40 -0400 Subject: [PATCH 1/3] otpform and otp generation -issue: how to save generated OTP to user, to check if user inputs the correct OTP --- .idea/inspectionProfiles/Project_Default.xml | 59 ++++++++++++++++++++ .idea/workspace.xml | 35 ++++++++++++ app/api/auth.py | 1 - app/auth/email.py | 23 ++++++++ app/auth/forms.py | 4 +- app/auth/routes.py | 14 +++-- app/main/forms.py | 1 - app/models.py | 7 +++ app/templates/auth/opt_login.html | 14 +++++ app/templates/email/otp.html | 9 +++ app/templates/email/otp.txt | 11 ++++ 11 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/workspace.xml create mode 100644 app/templates/auth/opt_login.html create mode 100644 app/templates/email/otp.html create mode 100644 app/templates/email/otp.txt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..a1f9af5 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,59 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..49100b4 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/api/auth.py b/app/api/auth.py index 230f98c..e78e282 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -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) diff --git a/app/auth/email.py b/app/auth/email.py index 98755ac..bdb1d7d 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -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)] + 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'), diff --git a/app/auth/forms.py b/app/auth/forms.py index c1dd3eb..f851b79 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -4,13 +4,15 @@ 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): + OTP = StringField(_l('One Time Passcode'), validators=[DataRequired()]) ###EqualTo(otp) + submit = SubmitField(_l('Log in') ) class RegistrationForm(FlaskForm): username = StringField(_l('Username'), validators=[DataRequired()]) diff --git a/app/auth/routes.py b/app/auth/routes.py index b3f1d72..77702dc 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -9,11 +9,8 @@ from app.auth.forms import LoginForm, RegistrationForm, \ 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,19 @@ 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.html') return redirect(next_page) return render_template('auth/login.html', title=_('Sign In'), form=form) +def auth_opt(): + form = OTPForm() + if OTP != user.curr_otp : + flash(_('Invalid OTP')) + 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(): diff --git a/app/main/forms.py b/app/main/forms.py index a7c8d96..f06209c 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -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')) diff --git a/app/models.py b/app/models.py index 5bdab20..c7741e7 100644 --- a/app/models.py +++ b/app/models.py @@ -96,6 +96,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), index=True, unique=True) 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 +119,12 @@ class User(UserMixin, PaginatedAPIMixin, db.Model): def __repr__(self): return ''.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) diff --git a/app/templates/auth/opt_login.html b/app/templates/auth/opt_login.html new file mode 100644 index 0000000..9ab59b3 --- /dev/null +++ b/app/templates/auth/opt_login.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

{{ _('Validation') }}

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+

{{ _('New User?') }} {{ _('Click to Register!') }}

+

{{ _('Returning User?') }} {{ _('Click to Login!') }}

+{% endblock %} diff --git a/app/templates/email/otp.html b/app/templates/email/otp.html new file mode 100644 index 0000000..a2460e2 --- /dev/null +++ b/app/templates/email/otp.html @@ -0,0 +1,9 @@ +

Dear {{ user.username }},

+

+ Here is your one time passcode +

{{otp}}

+

+ +

If you have not requested an otp, simply ignore this message.

+

Sincerely,

+

The Microblog Team

diff --git a/app/templates/email/otp.txt b/app/templates/email/otp.txt new file mode 100644 index 0000000..afc9182 --- /dev/null +++ b/app/templates/email/otp.txt @@ -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 From a2857083a8a902671c018dcdffe3136c76aba2fa Mon Sep 17 00:00:00 2001 From: Allyssa Poulin <40319407+allyssap@users.noreply.github.com> Date: Sun, 12 Mar 2023 20:22:05 -0400 Subject: [PATCH 2/3] otp password --- app/auth/routes.py | 6 +++--- app/models.py | 11 ++++++----- app/templates/.DS_Store | Bin 0 -> 6148 bytes .../auth/{opt_login.html => otp_login.html} | 0 migrations/versions/e517276bb1c2_users_table.py | 1 + 5 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 app/templates/.DS_Store rename app/templates/auth/{opt_login.html => otp_login.html} (100%) diff --git a/app/auth/routes.py b/app/auth/routes.py index 77702dc..6c83fb7 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -20,17 +20,17 @@ 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('auth/otp_login.html') + next_page = url_for('auth.otp_login') return redirect(next_page) return render_template('auth/login.html', title=_('Sign In'), form=form) -def auth_opt(): +@bp.route('/otp', methods=['GET', 'POST']) +def otp_login(): form = OTPForm() if OTP != user.curr_otp : flash(_('Invalid OTP')) if form.validate_on_submit(): return redirect(url_for('main.index')) - return render_template('auth/otp_login.html', title=_('Enter OTP'), form=form) diff --git a/app/models.py b/app/models.py index c7741e7..c725823 100644 --- a/app/models.py +++ b/app/models.py @@ -96,7 +96,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), index=True, unique=True) + 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) @@ -119,11 +119,12 @@ class User(UserMixin, PaginatedAPIMixin, db.Model): def __repr__(self): return ''.format(self.username) - def set_otp(self, otp): - self.otp = otp +## + ## def set_otp(self, otp): + ## self.otp = otp - def get_otp(self): - return self.otp + ## def get_otp(self): + ## return self.otp def set_password(self, password): self.password_hash = generate_password_hash(password) diff --git a/app/templates/.DS_Store b/app/templates/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1aa90dd38745402468a211e4eb99a4992b4db050 GIT binary patch literal 6148 zcmeH~K?=e^3`G;|qTr@Wm$UHz-e3?tK`)>n=t3%px}Kx^lL>;=wTS#c@+X-IrEk$` zL_}A&{Zgb8krr+$3kxGt4X|{^>yQ5dhkt?1r_^63}D?Xbvq^QGsbp4;roN zV~Ewg9h%}?4lPw{yJ!p_8c$Z6VqjX^MH3R3W)}t$Ab}BqY0W#k|2ObY^Z%%YDG89k zpApbz-LF@8skmF;UeD^=sM@-~p?)0U_8}^eepgFWuMFqwm0mr~V0$(NY E0L2Ru#Q*>R literal 0 HcmV?d00001 diff --git a/app/templates/auth/opt_login.html b/app/templates/auth/otp_login.html similarity index 100% rename from app/templates/auth/opt_login.html rename to app/templates/auth/otp_login.html diff --git a/migrations/versions/e517276bb1c2_users_table.py b/migrations/versions/e517276bb1c2_users_table.py index 4353272..5eb8181 100644 --- a/migrations/versions/e517276bb1c2_users_table.py +++ b/migrations/versions/e517276bb1c2_users_table.py @@ -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) From 0098f3448af96973307dabe25cc9f3829ee9a9af Mon Sep 17 00:00:00 2001 From: Allyssa Poulin <40319407+allyssap@users.noreply.github.com> Date: Sun, 12 Mar 2023 20:46:36 -0400 Subject: [PATCH 3/3] otp page working needs testing: does user receive an email does it check that the otp matches the generated one --- app/auth/email.py | 2 +- app/auth/forms.py | 3 ++- app/auth/routes.py | 13 ++++++++++--- app/models.py | 1 - 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/auth/email.py b/app/auth/email.py index bdb1d7d..e4a765c 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -9,11 +9,11 @@ def generate_otp(user): # 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): diff --git a/app/auth/forms.py b/app/auth/forms.py index f851b79..9a1af02 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -11,7 +11,8 @@ class LoginForm(FlaskForm): submit = SubmitField(_l('Sign In')) class OTPForm(FlaskForm): - OTP = StringField(_l('One Time Passcode'), validators=[DataRequired()]) ###EqualTo(otp) + username = StringField(_l('Username'), validators=[DataRequired()]) + OTP = StringField(_l('OTP'), validators=[DataRequired()]) ###EqualTo(otp) submit = SubmitField(_l('Log in') ) class RegistrationForm(FlaskForm): diff --git a/app/auth/routes.py b/app/auth/routes.py index 6c83fb7..84f280f 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -5,7 +5,7 @@ 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 @@ -27,8 +27,15 @@ def login(): @bp.route('/otp', methods=['GET', 'POST']) def otp_login(): form = OTPForm() - if OTP != user.curr_otp : - flash(_('Invalid OTP')) + 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'), diff --git a/app/models.py b/app/models.py index c725823..3a9dcae 100644 --- a/app/models.py +++ b/app/models.py @@ -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)