Merge pull request #94 from simple-login/social-login

Social login improvements
This commit is contained in:
Son Nguyen Kim 2020-02-27 23:02:02 +07:00 committed by GitHub
commit 4358b2791b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 11 deletions

View File

@ -45,6 +45,10 @@ def auth_login():
elif not user.activated: elif not user.activated:
return jsonify(error="Account not activated"), 400 return jsonify(error="Account not activated"), 400
return jsonify(**auth_payload(user, device)), 200
def auth_payload(user, device) -> dict:
ret = { ret = {
"name": user.name, "name": user.name,
"mfa_enabled": user.enable_otp, "mfa_enabled": user.enable_otp,
@ -64,4 +68,4 @@ def auth_login():
ret["mfa_key"] = None ret["mfa_key"] = None
ret["api_key"] = api_key.code ret["api_key"] = api_key.code
return jsonify(**ret), 200 return ret

View File

@ -14,7 +14,7 @@ from app.config import (
) )
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import User from app.models import User, SocialAuth
from .login_utils import after_login from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email, email_already_used 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 :) # reset the next_url to avoid user getting redirected at each login :)
session.pop("facebook_next_url", None) 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) return after_login(user, next_url)

View File

@ -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.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import User from app.models import User, SocialAuth
from app.utils import encode_url from app.utils import encode_url
_authorization_base_url = "https://github.com/login/oauth/authorize" _authorization_base_url = "https://github.com/login/oauth/authorize"
@ -105,6 +105,10 @@ def github_callback():
flash(f"Welcome to SimpleLogin {user.name}!", "success") 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 # The activation link contains the original page, for ex authorize page
next_url = request.args.get("next") if request.args else None next_url = request.args.get("next") if request.args else None

View File

@ -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.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, DISABLE_REGISTRATION
from app.extensions import db from app.extensions import db
from app.log import LOG 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 app.utils import random_string
from .login_utils import after_login from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email, email_already_used 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 :) # reset the next_url to avoid user getting redirected at each login :)
session.pop("google_next_url", None) session.pop("google_next_url", None)
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) return after_login(user, next_url)

View File

@ -102,8 +102,10 @@ class AliasGeneratorEnum(enum.Enum):
class User(db.Model, ModelMixin, UserMixin): class User(db.Model, ModelMixin, UserMixin):
__tablename__ = "users" __tablename__ = "users"
email = db.Column(db.String(256), unique=True, nullable=False) 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) salt = db.Column(db.String(128), nullable=True)
password = db.Column(db.String(128), nullable=True)
name = db.Column(db.String(128), nullable=False) name = db.Column(db.String(128), nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False) is_admin = db.Column(db.Boolean, nullable=False, default=False)
alias_generator = db.Column( alias_generator = db.Column(
@ -156,11 +158,9 @@ class User(db.Model, ModelMixin, UserMixin):
def create(cls, email, name, password=None, **kwargs): def create(cls, email, name, password=None, **kwargs):
user: User = super(User, cls).create(email=email, name=name, **kwargs) user: User = super(User, cls).create(email=email, name=name, **kwargs)
if not password: if password:
# set a random password
password = random_string(20)
user.set_password(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 # create a first alias mail to show user how to use when they login
@ -241,6 +241,8 @@ class User(db.Model, ModelMixin, UserMixin):
self.password = password_hash self.password = password_hash
def check_password(self, password) -> bool: def check_password(self, password) -> bool:
if not self.password:
return False
password_hash = bcrypt.hashpw(password.encode(), self.salt.encode()) password_hash = bcrypt.hashpw(password.encode(), self.salt.encode())
return self.password.encode() == password_hash return self.password.encode() == password_hash
@ -351,6 +353,17 @@ class ResetPasswordCode(db.Model, ModelMixin):
return self.expired < arrow.now() 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 >>> # <<< OAUTH models >>>

View File

@ -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 ###

View File

@ -39,8 +39,9 @@ def test_construct_url():
def test_authorize_page_non_login_user(flask_client): def test_authorize_page_non_login_user(flask_client):
"""make sure to display login page for non-authenticated user""" """make sure to display login page for non-authenticated user"""
user = User.create("test@test.com", "test 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() db.session.commit()
r = flask_client.get( r = flask_client.get(