control 3 年 前
コミット
78fcd0f1ee
9 ファイル変更105 行追加14 行削除
  1. 1 0
      Pipfile
  2. 12 4
      Pipfile.lock
  3. 42 6
      app/accounts.py
  4. BIN
      app/database.db
  5. 3 1
      app/forms.py
  6. 1 0
      app/models.py
  7. BIN
      app/static/repository/xtsQGBoMv1PW9bqr.jpeg
  8. 45 0
      app/templates/mfa.html
  9. 1 3
      main.py

+ 1 - 0
Pipfile

@@ -13,6 +13,7 @@ flask-talisman = "*"
 flask-wtf = "*"
 apscheduler = "*"
 stripe = "*"
+pyotp = "*"
 
 [dev-packages]
 

+ 12 - 4
Pipfile.lock

@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "1360cbb213fb2257619fc671fa456028c72f1ec9b4968a6086c7cc5691dff31c"
+            "sha256": "12d9a7fc2cb3712739da39dd4cb5818f40816fa6803069f2acdc13c3d724cf86"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -226,6 +226,14 @@
             "markers": "python_version >= '3.7'",
             "version": "==2.1.1"
         },
+        "pyotp": {
+            "hashes": [
+                "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28",
+                "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"
+            ],
+            "index": "pypi",
+            "version": "==2.6.0"
+        },
         "pytz": {
             "hashes": [
                 "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
@@ -309,11 +317,11 @@
         },
         "stripe": {
             "hashes": [
-                "sha256:7e3f50e844913e036f5419d73274315ae0d72315d36d3f791fdb572b1e84660c",
-                "sha256:ed8897f68e6bac3398cc998eb5634551840630d6504c0026fcfd0ad91c9a74a4"
+                "sha256:4b5ad1f0daff75e525bd93cadf176d7f4e73063c9fdc277a33f3cbbfec7cec3c",
+                "sha256:dfc004fc439fca5998b155414f2ab8e8ebb7589e4b7492653f4bac81008361c3"
             ],
             "index": "pypi",
-            "version": "==2.70.0"
+            "version": "==2.71.0"
         },
         "tzdata": {
             "hashes": [

+ 42 - 6
app/accounts.py

@@ -6,24 +6,28 @@ from werkzeug.security import generate_password_hash, check_password_hash
 from .models import User
 from . import db
 
-from .forms import LoginForm, RegForm
+from .forms import LoginForm, RegForm, MFAForm
+
+# MFA
+import pyotp
 
 accounts = Blueprint('accounts', __name__)
 
 @accounts.route('/login', methods=['GET', 'POST'])
 def login():
     form = LoginForm()
-    
+
     if form.validate_on_submit():
         email = form.email.data
         challenge_passwd = form.passwd.data
 
+        # Check fo user in User table
         user = User.query.filter_by(email=email).first()
+
+        # If there's a user
         if user:
             if check_password_hash(user.password, challenge_passwd):
-                flash('Successful Login!', category='success')
-                login_user(user, remember=True)
-                return redirect(url_for('dashboards.market'))
+                return redirect(url_for('accounts.mfa', user_chal = user.id)) # passes user to mfa
             else:
                 flash('Unsucessful Login!', category='error')
         else:
@@ -80,7 +84,39 @@ def register():
                     return redirect(url_for('dashboards.market'))
                 else:
                     flash('Registration Failed', category='error')
-    return render_template("register.html", user=current_user, form = form)
+    return render_template("register.html", user = current_user, form = form)
+
 
+@accounts.route('/mfa', methods=['GET', 'POST'])
+def mfa():
+    form = MFAForm()
+    user_chal = request.args['user_chal']
+    user = User.query.filter_by(id = user_chal).first()
+
+    # check for existing totphash
+    if not user.totphash:
+        # generate random secret key for auth
+        secret = pyotp.random_base32()
+        # add to User table and show this secret next time.
+        dbcall = User.query.filter_by(id = user.id).first()
+        dbcall.totphash = secret
+        db.session.commit()
+        flash('Generated new TOTP Secret', category='success')
+    else: # create a new totphash
+        secret = user.totphash
+
+    challenge_answer = int(pyotp.TOTP(secret).now())
+
+    if form.validate_on_submit():
+        otp = int(form.otp.data)
+        # checks MFA
+        if challenge_answer == otp:
+            flash('Login Successful!', category='sucess')
+            login_user(user, remember=True)
+            return redirect(url_for('dashboards.market'))
+        else:
+            flash('Login Unsuccessful!', category='error')
+            return redirect(url_for('accounts.mfa'))
 
 
+    return render_template('mfa.html', secret = secret, form = form, user = user)

BIN
app/database.db


+ 3 - 1
app/forms.py

@@ -44,4 +44,6 @@ class BidForm(FlaskForm):
     price = FloatField(validators=[DataRequired()])
     submit = SubmitField('Bid')
 
-
+class MFAForm(FlaskForm):
+    otp = StringField(validators=[DataRequired()])
+    submit = SubmitField('Authenticate')

+ 1 - 0
app/models.py

@@ -11,6 +11,7 @@ class User(db.Model, UserMixin): # User Database
     username = db.Column(db.String(150))
     profile_image = db.Column(db.String(150))
     focus = db.Column(db.Integer)
+    totphash = db.Column(db.String(32), unique=True)
 
 class TX(db.Model):
     id = db.Column(db.Integer, primary_key=True)

BIN
app/static/repository/xtsQGBoMv1PW9bqr.jpeg


+ 45 - 0
app/templates/mfa.html

@@ -0,0 +1,45 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+<div class="container">
+  <div class="row justify-content-center">
+    <div class="col-lg-12">
+      <div class="jumbotron text-center p-4">
+        <h2>Flask + 2FA Demo</h2>
+        <h4>Setup and Authenticate 2FA</h4>
+      </div>
+    </div>
+    <div class="col-lg-5">
+      <form>
+        <div>
+          <h5>Instructions!</h5>
+          <ul>
+            <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
+            <li>Create a new account with <strong>setup key</strong> method.</li>
+            <li>Provide the required details (name, secret key).</li>
+            <li>Select time-based authentication.</li>
+            <li>Submit the generated key in the form.</li>
+          </ul>
+        </div>
+        <div class="form-group">
+          <label for="secret">Secret Token</label>
+          <input type="text" class="form-control" id="secret" value="{{ secret }}" readonly>
+        </div>
+      </form>
+    </div>
+    <div class="col-lg-7">
+      <form method="POST">
+        {{ form.hidden_tag() }}
+        <div class="form-group">
+          <label for="otp">Generated OTP</label>
+          {{ form.otp() }}
+        </div>
+        <div class="text-center">
+          {{ form.submit() }}
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+{% endblock %}

+ 1 - 3
main.py

@@ -22,6 +22,4 @@ if __name__ == '__main__':
             sched.start()
     '''
 
-    app.run(debug=True) ## debug=True auto re-runs websever upon change turn off for production
-
-
+    app.run(debug=True)