From 7f70dd1678b593155ebb51e88b0ad99b24576f32 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:15:22 +0700 Subject: [PATCH 1/7] make User.password nullable, add SocialAuth model --- app/models.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/models.py b/app/models.py index 4d71fa11..41e985bc 100644 --- a/app/models.py +++ b/app/models.py @@ -103,7 +103,7 @@ class User(db.Model, ModelMixin, UserMixin): __tablename__ = "users" email = db.Column(db.String(256), unique=True, nullable=False) salt = db.Column(db.String(128), nullable=False) - password = db.Column(db.String(128), nullable=False) + password = db.Column(db.String(128), nullable=True) name = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, nullable=False, default=False) alias_generator = db.Column( @@ -156,12 +156,9 @@ class User(db.Model, ModelMixin, UserMixin): def create(cls, email, name, password=None, **kwargs): user: User = super(User, cls).create(email=email, name=name, **kwargs) - if not password: - # set a random password - password = random_string(20) - - user.set_password(password) - db.session.flush() + if password: + user.set_password(password) + db.session.flush() # create a first alias mail to show user how to use when they login GenEmail.create_new(user.id, prefix="my-first-alias") @@ -241,6 +238,8 @@ class User(db.Model, ModelMixin, UserMixin): self.password = password_hash def check_password(self, password) -> bool: + if not self.password: + return False password_hash = bcrypt.hashpw(password.encode(), self.salt.encode()) return self.password.encode() == password_hash @@ -351,6 +350,17 @@ class ResetPasswordCode(db.Model, ModelMixin): return self.expired < arrow.now() +class SocialAuth(db.Model, ModelMixin): + """Store how user authenticates with social login""" + + user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) + + # name of the social login used, could be facebook, google or github + social = db.Column(db.String(128), nullable=False) + + __table_args__ = (db.UniqueConstraint("user_id", "social", name="uq_social_auth"),) + + # <<< OAUTH models >>> From 87b6df9408847df17d35683e22062b8449095261 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:16:12 +0700 Subject: [PATCH 2/7] save which social network user uses in SocialAuth table --- app/auth/views/facebook.py | 6 +++++- app/auth/views/github.py | 6 +++++- app/auth/views/google.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/auth/views/facebook.py b/app/auth/views/facebook.py index b59beceb..e5a2cabd 100644 --- a/app/auth/views/facebook.py +++ b/app/auth/views/facebook.py @@ -14,7 +14,7 @@ from app.config import ( ) from app.extensions import db from app.log import LOG -from app.models import User +from app.models import User, SocialAuth from .login_utils import after_login from ...email_utils import can_be_used_as_personal_email, email_already_used @@ -143,4 +143,8 @@ def facebook_callback(): # reset the next_url to avoid user getting redirected at each login :) session.pop("facebook_next_url", None) + if not SocialAuth.get_by(user_id=user.id, social="facebook"): + SocialAuth.create(user_id=user.id, social="facebook") + db.session.commit() + return after_login(user, next_url) diff --git a/app/auth/views/github.py b/app/auth/views/github.py index 19ccbb0c..6a23ab0d 100644 --- a/app/auth/views/github.py +++ b/app/auth/views/github.py @@ -9,7 +9,7 @@ from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL, DISABLE_REGI from app.email_utils import can_be_used_as_personal_email, email_already_used from app.extensions import db from app.log import LOG -from app.models import User +from app.models import User, SocialAuth from app.utils import encode_url _authorization_base_url = "https://github.com/login/oauth/authorize" @@ -105,6 +105,10 @@ def github_callback(): flash(f"Welcome to SimpleLogin {user.name}!", "success") + if not SocialAuth.get_by(user_id=user.id, social="github"): + SocialAuth.create(user_id=user.id, social="github") + db.session.commit() + # The activation link contains the original page, for ex authorize page next_url = request.args.get("next") if request.args else None diff --git a/app/auth/views/google.py b/app/auth/views/google.py index 78792e60..5639b1ec 100644 --- a/app/auth/views/google.py +++ b/app/auth/views/google.py @@ -7,7 +7,7 @@ from app.auth.base import auth_bp from app.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, DISABLE_REGISTRATION from app.extensions import db from app.log import LOG -from app.models import User, File +from app.models import User, File, SocialAuth from app.utils import random_string from .login_utils import after_login from ...email_utils import can_be_used_as_personal_email, email_already_used @@ -128,6 +128,10 @@ def google_callback(): # reset the next_url to avoid user getting redirected at each login :) session.pop("google_next_url", None) + if not SocialAuth.get_by(user_id=user.id, social="github"): + SocialAuth.create(user_id=user.id, social="github") + db.session.commit() + return after_login(user, next_url) From 659861198bd91243e0018a720627fd7d2b385d03 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:18:26 +0700 Subject: [PATCH 3/7] make salt is nullable --- app/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 41e985bc..f9598f4a 100644 --- a/app/models.py +++ b/app/models.py @@ -102,8 +102,10 @@ class AliasGeneratorEnum(enum.Enum): class User(db.Model, ModelMixin, UserMixin): __tablename__ = "users" email = db.Column(db.String(256), unique=True, nullable=False) - salt = db.Column(db.String(128), nullable=False) + + salt = db.Column(db.String(128), nullable=True) password = db.Column(db.String(128), nullable=True) + name = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, nullable=False, default=False) alias_generator = db.Column( From 9d23fc5ab3f9108a0c25a0addea70dd0dca39561 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:26:29 +0700 Subject: [PATCH 4/7] small refacto --- app/api/views/auth_login.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/api/views/auth_login.py b/app/api/views/auth_login.py index 55ba87b0..59e78c1c 100644 --- a/app/api/views/auth_login.py +++ b/app/api/views/auth_login.py @@ -45,6 +45,10 @@ def auth_login(): elif not user.activated: return jsonify(error="Account not activated"), 400 + return jsonify(**auth_payload(user, device)), 200 + + +def auth_payload(user, device) -> dict: ret = { "name": user.name, "mfa_enabled": user.enable_otp, @@ -64,4 +68,4 @@ def auth_login(): ret["mfa_key"] = None ret["api_key"] = api_key.code - return jsonify(**ret), 200 + return ret From ed11363b0c2f351017df7cfd33aaef2395e12a63 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:26:35 +0700 Subject: [PATCH 5/7] add db migration script --- .../versions/2020_022722_75093e7ded27_.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 migrations/versions/2020_022722_75093e7ded27_.py diff --git a/migrations/versions/2020_022722_75093e7ded27_.py b/migrations/versions/2020_022722_75093e7ded27_.py new file mode 100644 index 00000000..b7a0f9b9 --- /dev/null +++ b/migrations/versions/2020_022722_75093e7ded27_.py @@ -0,0 +1,50 @@ +"""empty message + +Revision ID: 75093e7ded27 +Revises: e3cb44b953f2 +Create Date: 2020-02-27 22:26:25.068117 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '75093e7ded27' +down_revision = 'e3cb44b953f2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('social_auth', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False), + sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('social', sa.String(length=128), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'social', name='uq_social_auth') + ) + op.alter_column('users', 'password', + existing_type=sa.VARCHAR(length=128), + nullable=True) + op.alter_column('users', 'salt', + existing_type=sa.VARCHAR(length=128), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'salt', + existing_type=sa.VARCHAR(length=128), + nullable=False) + op.alter_column('users', 'password', + existing_type=sa.VARCHAR(length=128), + nullable=False) + op.drop_table('social_auth') + # ### end Alembic commands ### From 91d5d1c9aca78a2a99527d77e91fb34ea52ce52b Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:31:38 +0700 Subject: [PATCH 6/7] fix test --- app/models.py | 3 ++- tests/oauth/test_authorize.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index f9598f4a..14c1384a 100644 --- a/app/models.py +++ b/app/models.py @@ -160,7 +160,8 @@ class User(db.Model, ModelMixin, UserMixin): if password: user.set_password(password) - db.session.flush() + + db.session.flush() # create a first alias mail to show user how to use when they login GenEmail.create_new(user.id, prefix="my-first-alias") diff --git a/tests/oauth/test_authorize.py b/tests/oauth/test_authorize.py index 3620cade..196d5cca 100644 --- a/tests/oauth/test_authorize.py +++ b/tests/oauth/test_authorize.py @@ -39,8 +39,9 @@ def test_construct_url(): def test_authorize_page_non_login_user(flask_client): """make sure to display login page for non-authenticated user""" user = User.create("test@test.com", "test user") - client = Client.create_new("test client", user.id) + db.session.commit() + client = Client.create_new("test client", user.id) db.session.commit() r = flask_client.get( From 16718806bad412c9585edc8233183634fd5d12c6 Mon Sep 17 00:00:00 2001 From: Son NK Date: Thu, 27 Feb 2020 22:57:37 +0700 Subject: [PATCH 7/7] fix --- app/auth/views/google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/auth/views/google.py b/app/auth/views/google.py index 5639b1ec..96e4e53c 100644 --- a/app/auth/views/google.py +++ b/app/auth/views/google.py @@ -128,8 +128,8 @@ def google_callback(): # reset the next_url to avoid user getting redirected at each login :) session.pop("google_next_url", None) - if not SocialAuth.get_by(user_id=user.id, social="github"): - SocialAuth.create(user_id=user.id, social="github") + if not SocialAuth.get_by(user_id=user.id, social="google"): + SocialAuth.create(user_id=user.id, social="google") db.session.commit() return after_login(user, next_url)