Skip to content

Commit

Permalink
Merge pull request #47 from kenny-niu/two-factor-authentication
Browse files Browse the repository at this point in the history
Add Simple Two Factor Authentication
kenny-niu authored Dec 1, 2022
2 parents c25032c + 51f4734 commit 9300f94
Showing 8 changed files with 273 additions and 64 deletions.
38 changes: 38 additions & 0 deletions oxe-api/migrations/versions/b1327fd4d9d7_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""empty message
Revision ID: b1327fd4d9d7
Revises: 8692e9512214
Create Date: 2022-12-01 06:33:42.361181
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql


# revision identifiers, used by Alembic.
revision = 'b1327fd4d9d7'
down_revision = '8692e9512214'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('UserOtp',
sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
sa.Column('token', mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_unicode_ci', length=110), nullable=False),
sa.Column('timestamp', sa.DATETIME(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),

sa.Column('user_id', mysql.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['User.id'], name='otp_relationship_upfk_1', ondelete='CASCADE'),

sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', name='otp_relationship_type_unique_user'),
mysql_collate='utf8mb4_unicode_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)


def downgrade():
op.drop_table('UserOtp')
42 changes: 32 additions & 10 deletions oxe-api/resource/account/login.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import datetime

from flask import session, render_template
from flask_apispec import MethodResource
from flask_apispec import use_kwargs, doc
from flask_bcrypt import check_password_hash
from flask_jwt_extended import create_access_token, create_refresh_token
from flask_restful import Resource
from utils.token import generate_otp
from utils.mail import send_email
from webargs import fields

from decorator.catch_exception import catch_exception
@@ -14,9 +17,11 @@
class Login(MethodResource, Resource):

db = None
mail = None

def __init__(self, db):
def __init__(self, db, mail):
self.db = db
self.mail = mail

@log_request
@doc(tags=['account'],
@@ -44,13 +49,30 @@ def post(self, **kwargs):
if not data[0].is_admin and data[0].status == "NEW":
return "", "401 This account is not active. Please check your email for an activation link."

access_token_expires = datetime.timedelta(days=1)
refresh_token_expires = datetime.timedelta(days=365)
access_token = create_access_token(identity=str(data[0].id), expires_delta=access_token_expires)
refresh_token = create_refresh_token(identity=str(data[0].id), expires_delta=refresh_token_expires)
# delete old otp if exists
old_otp = self.db.get(self.db.tables["UserOtp"], {"user_id": data[0].id})
if len(old_otp) > 0:
self.db.delete(self.db.tables["UserOtp"], {"user_id": data[0].id})

return {
"user": data[0].id,
"access_token": access_token,
"refresh_token": refresh_token,
}, "200 "
# create new otp
otp = self.db.insert({
"token": generate_otp(),
"user_id": data[0].id,
}, self.db.tables["UserOtp"])

# send otp to user
send_email(self.mail,
subject=f"Login One Time Pin",
recipients=[kwargs["email"]],
html_body=render_template(
'login_otp.html',
token=otp.token,
)
)

# access_token_expires = datetime.timedelta(days=1)
# refresh_token_expires = datetime.timedelta(days=365)
# access_token = create_access_token(identity=str(data[0].id), expires_delta=access_token_expires)
# refresh_token = create_refresh_token(identity=str(data[0].id), expires_delta=refresh_token_expires)

return {}, "200 "
65 changes: 65 additions & 0 deletions oxe-api/resource/account/verify_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import datetime
from flask import request
from flask_apispec import MethodResource
from flask_apispec import use_kwargs, doc
from flask_restful import Resource
from flask_jwt_extended import create_access_token, create_refresh_token
from webargs import fields

from decorator.catch_exception import catch_exception

class VerifyLogin(MethodResource, Resource):

db = None
mail = None

def __init__(self, db, mail):
self.db = db
self.mail = mail

@doc(tags=['account'],
description='Verify login token',
responses={
"200": {},
"422.a": {"description": "This one time pin is invalid"},
"422.b": {"description": "This one time pin has expired"},
})
@use_kwargs({
'email': fields.Str(),
'token': fields.Str(),
})
@catch_exception
def post(self, **kwargs):

if 'HTTP_ORIGIN' in request.environ and request.environ['HTTP_ORIGIN']:
origin = request.environ['HTTP_ORIGIN']
else:
return "", "500 Impossible to find the origin. Please contact the administrator"

user = self.db.get(self.db.tables["User"], {"email": kwargs["email"]})
otp = self.db.get(self.db.tables["UserOtp"], {"user_id": user[0].id})

if len(otp) < 1:
return "", "422 This one time pin is invalid."

# Verify token
if otp[0].token != kwargs["token"]:
return "", "422 This one time pin is invalid."
token_timestamp = otp[0].timestamp.timestamp()
now_timestamp = datetime.datetime.now().timestamp()
if now_timestamp - token_timestamp > 600:
return "", "422 This one time pin has expired."

# delete token if valid
self.db.delete(self.db.tables["UserOtp"], {"id": otp[0].id})

access_token_expires = datetime.timedelta(days=1)
refresh_token_expires = datetime.timedelta(days=365)
access_token = create_access_token(identity=str(user[0].id), expires_delta=access_token_expires)
refresh_token = create_refresh_token(identity=str(user[0].id), expires_delta=refresh_token_expires)

return {
"user": user[0].id,
"access_token": access_token,
"refresh_token": refresh_token,
}, "200 "
1 change: 1 addition & 0 deletions oxe-api/routes.py
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
from resource.account.verify_account import VerifyAccount
from resource.account.update_status import UpdateStatus
from resource.account.add_profile import AddProfile
from resource.account.verify_login import VerifyLogin
from resource.analytics.get_ecosystem_activity import GetEcosystemActivity
from resource.article.copy_article_version import CopyArticleVersion
from resource.article.get_articles import GetArticles
13 changes: 13 additions & 0 deletions oxe-api/template/login_otp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<p>Dear {{ email }},</p>
<p>To complete your login, please enter the following One Time Pin:</p>

<p style="background: #fefefe; padding: 10px; border-radius: 5px; font-size: 20px;">
{{ token|safe }}
</p>

<span style="font-style: italic; font-size: 0.8rem;">This code expires in 10 minutes.</span>

<p>Sincerely,</p>
<p>
<img src="cid:logo" alt="Logo" style="width:200px;">
</p>
9 changes: 8 additions & 1 deletion oxe-api/utils/token.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
from itsdangerous import URLSafeTimedSerializer

from config.config import SECRET_KEY, SECURITY_SALT
@@ -14,4 +15,10 @@ def confirm_token(token, expiration=3600):
token,
salt=SECURITY_SALT,
max_age=expiration
)
)

def generate_otp(otp_size = 6):
final_otp = ''
for _ in range(otp_size):
final_otp = final_otp + str(random.randint(0,9))
return final_otp
Loading

0 comments on commit 9300f94

Please sign in to comment.