From a774cc8da32dfaa643340b517e3e5cbbea04a9d8 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Mon, 10 Aug 2020 20:07:56 +0530 Subject: [PATCH 01/20] google login done --- app/api/dao/user.py | 21 ++++++++++++++ app/api/models/user.py | 9 ++++++ app/api/resources/user.py | 59 +++++++++++++++++++++++++++++++++++++++ app/messages.py | 1 + 4 files changed, 90 insertions(+) diff --git a/app/api/dao/user.py b/app/api/dao/user.py index dbec23e1f..85bb26462 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -377,6 +377,27 @@ def authenticate(username_or_email: str, password: str): return None + @staticmethod + def get_user_for_google_login(email: str): + """Returns user for google login + + Checks email of the user to find an existing user. + If found, the user is returned. + Else, a new user is created and returned + + Arguments: + email: email of the user, used to check for existing user + + Returns: + Returns authenticated user + """ + user = UserModel.find_by_email(email) + + if user: + return user + else: + return None + @staticmethod @email_verification_required def get_achievements(user_id: int): diff --git a/app/api/models/user.py b/app/api/models/user.py index bcf6d4ad2..49d36a9eb 100644 --- a/app/api/models/user.py +++ b/app/api/models/user.py @@ -16,6 +16,7 @@ def add_models_to_namespace(api_namespace): update_user_request_body_model.name ] = update_user_request_body_model api_namespace.models[login_request_body_model.name] = login_request_body_model + api_namespace.models[google_auth_body_model.name] = google_auth_body_model api_namespace.models[login_response_body_model.name] = login_response_body_model api_namespace.models[refresh_response_body_model.name] = refresh_response_body_model api_namespace.models[ @@ -152,6 +153,14 @@ def add_models_to_namespace(api_namespace): }, ) +google_auth_body_model = Model( + "Google authentication data model", + { + "id_token": fields.String(required=True, description="User's idToken given by Google auth"), + "email": fields.String(required=True, description="User's email given by Google auth"), + }, +) + # TODO: Remove 'expiry' after the android app refactoring. login_response_body_model = Model( "Login response data model", diff --git a/app/api/resources/user.py b/app/api/resources/user.py index bdf5a0b62..1e93c1b15 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -16,6 +16,8 @@ from app.api.models.user import * from app.api.dao.user import UserDAO from app.api.resources.common import auth_header_parser +from google.oauth2 import id_token +from google.auth.transport import requests users_ns = Namespace("Users", description="Operations related to users") add_models_to_namespace(users_ns) @@ -389,6 +391,63 @@ def post(cls): HTTPStatus.OK, ) +@users_ns.route("google/auth/callback") +class GoogleAuth(Resource): + @classmethod + @users_ns.doc("google-auth callback") + @users_ns.response(HTTPStatus.OK, "Successful login", login_response_body_model) + @users_ns.response(HTTPStatus.UNAUTHORIZED, f"{messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED}") + @users_ns.response(HTTPStatus.NOT_FOUND, f"{messages.USER_NOT_FOUND}") + @users_ns.expect(google_auth_body_model) + def post(cls): + """ + Login/Sign-in user using Google Sign-In. + + The Google user idToken is received from the client side, which is then verified for its authenticity. + On verification, the user is either logged-in or sign-up depending upon wheter it is an existing + or a new user. + """ + + token = request.json.get("id_token") + email = request.json.get("email") + client_id = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com" + + # Verify google auth id token + try: + idinfo = id_token.verify_oauth2_token(token, requests.Request(), client_id) + + # id_token is valid. Get user. + user = DAO.get_user_for_google_login(email) + + if not user: + return messages.USER_NOT_FOUND, HTTPStatus.NOT_FOUND + + # create tokens and expiry timestamps + access_token = create_access_token(identity=user.id) + refresh_token = create_refresh_token(identity=user.id) + + from run import application + access_expiry = datetime.utcnow() + application.config.get( + "JWT_ACCESS_TOKEN_EXPIRES" + ) + refresh_expiry = datetime.utcnow() + application.config.get( + "JWT_REFRESH_TOKEN_EXPIRES" + ) + + # return data + return ( + { + "access_token": access_token, + "access_expiry": access_expiry.timestamp(), + "refresh_token": refresh_token, + "refresh_expiry": refresh_expiry.timestamp(), + }, + HTTPStatus.OK, + ) + + except ValueError: + return messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED, HTTPStatus.UNAUTHORIZED + @users_ns.route("login") class LoginUser(Resource): diff --git a/app/messages.py b/app/messages.py index 83f3f538d..6df51d72a 100644 --- a/app/messages.py +++ b/app/messages.py @@ -205,6 +205,7 @@ EMAIL_VERIFICATION_MESSAGE = { "message": "Check your email, a new verification" " email was sent." } +GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED = "Google auth token verification failed" # Success messages TASK_WAS_ALREADY_ACHIEVED = {"message": "Task was already achieved."} From fb6408f9b6387d9da750404c58be41d0065b1ae3 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Tue, 11 Aug 2020 15:06:50 +0530 Subject: [PATCH 02/20] requirements updated --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 24cee1fcb..8816f9a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,7 @@ Flask-SQLAlchemy==2.3.2 Flask-Testing==0.7.1 future==0.16.0 gunicorn==20.0.4 +google-auth==1.20.1 idna==2.6 itsdangerous==0.24 Jinja2==2.10 From 9b8cedf90e9ed327581944e1f964b71622045ac2 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Tue, 11 Aug 2020 16:44:36 +0530 Subject: [PATCH 03/20] sign up using google added (new user is created if old user not found for email id) --- app/api/dao/user.py | 24 ++++++++++++++++++++++++ app/api/models/user.py | 5 +++-- app/api/resources/user.py | 4 +++- app/database/models/user.py | 10 +++++++--- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/api/dao/user.py b/app/api/dao/user.py index 85bb26462..3f28969ed 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -69,6 +69,30 @@ def create_user(data: Dict[str, str]): return messages.USER_WAS_CREATED_SUCCESSFULLY, HTTPStatus.CREATED + @staticmethod + def create_user_using_google(data: Dict[str, str]): + """ + Creates a new user using Google Auth. + + Arguments: + data: A list containing the user's name and email + + Returns: + The new user created + """ + + name = data["name"] + username = None + password = None + email = data["email"] + terms_and_conditions_checked = True + social_login = True + + user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login) + user.save_to_db() + + return user + @staticmethod @email_verification_required def delete_user(user_id: int): diff --git a/app/api/models/user.py b/app/api/models/user.py index 49d36a9eb..70710ba54 100644 --- a/app/api/models/user.py +++ b/app/api/models/user.py @@ -120,8 +120,8 @@ def add_models_to_namespace(api_namespace): "User registration model", { "name": fields.String(required=True, description="User name"), - "username": fields.String(required=True, description="User username"), - "password": fields.String(required=True, description="User password"), + "username": fields.String(required=False, description="User username"), + "password": fields.String(required=False, description="User password"), "email": fields.String(required=True, description="User email"), "terms_and_conditions_checked": fields.Boolean( required=True, description="User check Terms and Conditions value" @@ -157,6 +157,7 @@ def add_models_to_namespace(api_namespace): "Google authentication data model", { "id_token": fields.String(required=True, description="User's idToken given by Google auth"), + "name": fields.String(required=True, description="User's name given by Google auth"), "email": fields.String(required=True, description="User's email given by Google auth"), }, ) diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 1e93c1b15..4b1e143a1 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -420,7 +420,9 @@ def post(cls): user = DAO.get_user_for_google_login(email) if not user: - return messages.USER_NOT_FOUND, HTTPStatus.NOT_FOUND + # create a new user + data = request.json + user = DAO.create_user_using_google(data) # create tokens and expiry timestamps access_token = create_access_token(identity=user.id) diff --git a/app/database/models/user.py b/app/database/models/user.py index 2f8ee9985..ea7cd837c 100644 --- a/app/database/models/user.py +++ b/app/database/models/user.py @@ -59,7 +59,7 @@ class UserModel(db.Model): need_mentoring = db.Column(db.Boolean) available_to_mentor = db.Column(db.Boolean) - def __init__(self, name, username, password, email, terms_and_conditions_checked): + def __init__(self, name, username, password, email, terms_and_conditions_checked, social_login=False): """Initialises userModel class with name, username, password, email, and terms_and_conditions_checked. """ ## required fields @@ -69,11 +69,15 @@ def __init__(self, name, username, password, email, terms_and_conditions_checked self.terms_and_conditions_checked = terms_and_conditions_checked # saving hash instead of saving password in plain text - self.set_password(password) + if not social_login: + self.set_password(password) # default values self.is_admin = True if self.is_empty() else False # first user is admin - self.is_email_verified = False + if social_login: + self.is_email_verified = True + else: + self.is_email_verified = False self.registration_date = time.time() ## optional fields From 4cea797735e273d7891d2845256db83ea78ef19b Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Thu, 13 Aug 2020 02:56:33 +0530 Subject: [PATCH 04/20] apple sign in done --- app/api/dao/user.py | 18 ++++++++--- app/api/models/user.py | 4 +-- app/api/resources/user.py | 61 ++++++++++++++++++++++++++++++++++--- app/database/models/user.py | 12 +++++++- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/app/api/dao/user.py b/app/api/dao/user.py index 3f28969ed..e349b33c5 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -70,7 +70,7 @@ def create_user(data: Dict[str, str]): return messages.USER_WAS_CREATED_SUCCESSFULLY, HTTPStatus.CREATED @staticmethod - def create_user_using_google(data: Dict[str, str]): + def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None): """ Creates a new user using Google Auth. @@ -88,7 +88,7 @@ def create_user_using_google(data: Dict[str, str]): terms_and_conditions_checked = True social_login = True - user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login) + user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login, apple_auth_id) user.save_to_db() return user @@ -402,7 +402,7 @@ def authenticate(username_or_email: str, password: str): return None @staticmethod - def get_user_for_google_login(email: str): + def get_user_for_social_login(email: str, apple_auth_id: str=None): """Returns user for google login Checks email of the user to find an existing user. @@ -415,7 +415,17 @@ def get_user_for_google_login(email: str): Returns: Returns authenticated user """ - user = UserModel.find_by_email(email) + + # If apple auth id given, first try to find existing user using it + if apple_auth_id: + user = UserModel.find_by_apple_auth_id(apple_auth_id) + # if user not found, try finding using email + if not user: + user = UserModel.find_by_email(email) + + # If apple auth id not given, try finding using email + else: + user = UserModel.find_by_email(email) if user: return user diff --git a/app/api/models/user.py b/app/api/models/user.py index 70710ba54..785807ad4 100644 --- a/app/api/models/user.py +++ b/app/api/models/user.py @@ -16,7 +16,7 @@ def add_models_to_namespace(api_namespace): update_user_request_body_model.name ] = update_user_request_body_model api_namespace.models[login_request_body_model.name] = login_request_body_model - api_namespace.models[google_auth_body_model.name] = google_auth_body_model + api_namespace.models[social_auth_body_model.name] = social_auth_body_model api_namespace.models[login_response_body_model.name] = login_response_body_model api_namespace.models[refresh_response_body_model.name] = refresh_response_body_model api_namespace.models[ @@ -153,7 +153,7 @@ def add_models_to_namespace(api_namespace): }, ) -google_auth_body_model = Model( +social_auth_body_model = Model( "Google authentication data model", { "id_token": fields.String(required=True, description="User's idToken given by Google auth"), diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 4b1e143a1..e81a608dd 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -391,14 +391,67 @@ def post(cls): HTTPStatus.OK, ) + +@users_ns.route("apple/auth/callback") +class AppleAuth(Resource): + @classmethod + @users_ns.doc("apple-auth callback") + @users_ns.response(HTTPStatus.OK, "Successful login", login_response_body_model) + @users_ns.expect(social_auth_body_model) + def post(cls): + """ + Login/Sign-in user using Apple Sign-In. + + The Apple user id (id_token) is recieved which becomes the primary basis for user identification. + If not found then that means user is using apple sign-in for first time. In this case, email is checked. + If email found, that account is used. Else, a new account is created. + """ + + apple_auth_id = request.json.get("id_token") + email = request.json.get("email") + + # get existing user + user = DAO.get_user_for_social_login(email, apple_auth_id) + + # if user not found, create a new user + if not user: + data = request.json + user = DAO.create_user_using_social_login(data, apple_auth_id) + # else, set apple auth id to later identify the user + else: + user.apple_auth_id = apple_auth_id + user.save_to_db() + + # create tokens and expiry timestamps + access_token = create_access_token(identity=user.id) + refresh_token = create_refresh_token(identity=user.id) + + from run import application + access_expiry = datetime.utcnow() + application.config.get( + "JWT_ACCESS_TOKEN_EXPIRES" + ) + refresh_expiry = datetime.utcnow() + application.config.get( + "JWT_REFRESH_TOKEN_EXPIRES" + ) + + # return data + return ( + { + "access_token": access_token, + "access_expiry": access_expiry.timestamp(), + "refresh_token": refresh_token, + "refresh_expiry": refresh_expiry.timestamp(), + }, + HTTPStatus.OK, + ) + @users_ns.route("google/auth/callback") class GoogleAuth(Resource): @classmethod @users_ns.doc("google-auth callback") @users_ns.response(HTTPStatus.OK, "Successful login", login_response_body_model) @users_ns.response(HTTPStatus.UNAUTHORIZED, f"{messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED}") - @users_ns.response(HTTPStatus.NOT_FOUND, f"{messages.USER_NOT_FOUND}") - @users_ns.expect(google_auth_body_model) + @users_ns.expect(social_auth_body_model) def post(cls): """ Login/Sign-in user using Google Sign-In. @@ -417,12 +470,12 @@ def post(cls): idinfo = id_token.verify_oauth2_token(token, requests.Request(), client_id) # id_token is valid. Get user. - user = DAO.get_user_for_google_login(email) + user = DAO.get_user_for_social_login(email) if not user: # create a new user data = request.json - user = DAO.create_user_using_google(data) + user = DAO.create_user_using_social_login(data) # create tokens and expiry timestamps access_token = create_access_token(identity=user.id) diff --git a/app/database/models/user.py b/app/database/models/user.py index ea7cd837c..00c0004fd 100644 --- a/app/database/models/user.py +++ b/app/database/models/user.py @@ -29,6 +29,7 @@ class UserModel(db.Model): # security password_hash = db.Column(db.String(100)) + apple_auth_id = db.Column(db.String(100)) # registration registration_date = db.Column(db.Float) @@ -59,7 +60,7 @@ class UserModel(db.Model): need_mentoring = db.Column(db.Boolean) available_to_mentor = db.Column(db.Boolean) - def __init__(self, name, username, password, email, terms_and_conditions_checked, social_login=False): + def __init__(self, name, username, password, email, terms_and_conditions_checked, social_login=False, apple_auth_id=None): """Initialises userModel class with name, username, password, email, and terms_and_conditions_checked. """ ## required fields @@ -72,6 +73,10 @@ def __init__(self, name, username, password, email, terms_and_conditions_checked if not social_login: self.set_password(password) + # save auth id for sign in with apple (if present) + if apple_auth_id: + self.apple_auth_id = apple_auth_id + # default values self.is_admin = True if self.is_empty() else False # first user is admin if social_login: @@ -133,6 +138,11 @@ def find_by_id(cls, _id: int) -> 'UserModel': """Returns the user that has the id we searched for. """ return cls.query.filter_by(id=_id).first() + @classmethod + def find_by_apple_auth_id(cls, _id: str) -> 'UserModel': + """Returns the user that has apple auth id we searched for.""" + return cls.query.filter_by(apple_auth_id=_id).first() + @classmethod def get_all_admins(cls, is_admin=True): """Returns all the admins. """ From 6340c571f666095611bd7f882e515188b508bd11 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Thu, 13 Aug 2020 11:01:24 +0530 Subject: [PATCH 05/20] creating and returning access tokens code refactored --- app/api/resources/user.py | 92 +++++++++++---------------------------- 1 file changed, 26 insertions(+), 66 deletions(-) diff --git a/app/api/resources/user.py b/app/api/resources/user.py index e81a608dd..08ea8ae83 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -24,6 +24,29 @@ DAO = UserDAO() # User data access object +def create_tokens_for_new_user_and_return(user): + # create tokens and expiry timestamps + access_token = create_access_token(identity=user.id) + refresh_token = create_refresh_token(identity=user.id) + + from run import application + access_expiry = datetime.utcnow() + application.config.get( + "JWT_ACCESS_TOKEN_EXPIRES" + ) + refresh_expiry = datetime.utcnow() + application.config.get( + "JWT_REFRESH_TOKEN_EXPIRES" + ) + + # return data + return ( + { + "access_token": access_token, + "access_expiry": access_expiry.timestamp(), + "refresh_token": refresh_token, + "refresh_expiry": refresh_expiry.timestamp(), + }, + HTTPStatus.OK, + ) @users_ns.route("users") @users_ns.response( @@ -422,28 +445,7 @@ def post(cls): user.apple_auth_id = apple_auth_id user.save_to_db() - # create tokens and expiry timestamps - access_token = create_access_token(identity=user.id) - refresh_token = create_refresh_token(identity=user.id) - - from run import application - access_expiry = datetime.utcnow() + application.config.get( - "JWT_ACCESS_TOKEN_EXPIRES" - ) - refresh_expiry = datetime.utcnow() + application.config.get( - "JWT_REFRESH_TOKEN_EXPIRES" - ) - - # return data - return ( - { - "access_token": access_token, - "access_expiry": access_expiry.timestamp(), - "refresh_token": refresh_token, - "refresh_expiry": refresh_expiry.timestamp(), - }, - HTTPStatus.OK, - ) + return create_tokens_for_new_user_and_return(user) @users_ns.route("google/auth/callback") class GoogleAuth(Resource): @@ -477,28 +479,7 @@ def post(cls): data = request.json user = DAO.create_user_using_social_login(data) - # create tokens and expiry timestamps - access_token = create_access_token(identity=user.id) - refresh_token = create_refresh_token(identity=user.id) - - from run import application - access_expiry = datetime.utcnow() + application.config.get( - "JWT_ACCESS_TOKEN_EXPIRES" - ) - refresh_expiry = datetime.utcnow() + application.config.get( - "JWT_REFRESH_TOKEN_EXPIRES" - ) - - # return data - return ( - { - "access_token": access_token, - "access_expiry": access_expiry.timestamp(), - "refresh_token": refresh_token, - "refresh_expiry": refresh_expiry.timestamp(), - }, - HTTPStatus.OK, - ) + return create_tokens_for_new_user_and_return(user) except ValueError: return messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED, HTTPStatus.UNAUTHORIZED @@ -545,28 +526,7 @@ def post(cls): if not user.is_email_verified: return messages.USER_HAS_NOT_VERIFIED_EMAIL_BEFORE_LOGIN, HTTPStatus.FORBIDDEN - access_token = create_access_token(identity=user.id) - refresh_token = create_refresh_token(identity=user.id) - - from run import application - - access_expiry = datetime.utcnow() + application.config.get( - "JWT_ACCESS_TOKEN_EXPIRES" - ) - refresh_expiry = datetime.utcnow() + application.config.get( - "JWT_REFRESH_TOKEN_EXPIRES" - ) - - return ( - { - "access_token": access_token, - "access_expiry": access_expiry.timestamp(), - "refresh_token": refresh_token, - "refresh_expiry": refresh_expiry.timestamp(), - }, - HTTPStatus.OK, - ) - + return create_tokens_for_new_user_and_return(user) @users_ns.route("home") @users_ns.doc("home") From dd9216ad656af66e4afeaaab70f9650dfb5bab4f Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Fri, 14 Aug 2020 02:16:13 +0530 Subject: [PATCH 06/20] social sign in table created and used --- app/api/dao/user.py | 58 ++++++++++++--------------- app/api/resources/user.py | 20 ++++++--- app/database/models/social_sign_in.py | 41 +++++++++++++++++++ app/database/models/user.py | 12 +----- app/messages.py | 1 + 5 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 app/database/models/social_sign_in.py diff --git a/app/api/dao/user.py b/app/api/dao/user.py index e349b33c5..0ff1acd7e 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -10,6 +10,7 @@ from app.api.email_utils import confirm_token from app.database.models.mentorship_relation import MentorshipRelationModel from app.database.models.user import UserModel +from app.database.models.social_sign_in import SocialSignInModel from app.utils.decorator_utils import email_verification_required from app.utils.enum_utils import MentorshipRelationState from app.database.models.mentorship_relation import MentorshipRelationModel @@ -88,11 +89,35 @@ def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None terms_and_conditions_checked = True social_login = True - user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login, apple_auth_id) + # create and save user + user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login) user.save_to_db() + # create and save social sign in details for the user + user_id = UserModel.find_by_email(email).id + social_sign_in_type = "apple" if apple_auth_id else "google" + id_token = data["id_token"] + social_sign_in_details = SocialSignInModel(user_id, social_sign_in_type, id_token, email, name) + social_sign_in_details.save_to_db() + return user + @staticmethod + def get_social_sign_in_details(user_id: int, social_sign_in_type: str): + """ + Returns social sign in details of the user. + + Arguments: + user_id: user_id whose details are to be fetched + social_sign_in_type: social sign in type whole details are to be fetched + + Returns: + social sign in details of the user for the specificed type + """ + + social_sign_in_details = SocialSignInModel.get_social_sign_in_details(user_id, social_sign_in_type) + return social_sign_in_details + @staticmethod @email_verification_required def delete_user(user_id: int): @@ -401,37 +426,6 @@ def authenticate(username_or_email: str, password: str): return None - @staticmethod - def get_user_for_social_login(email: str, apple_auth_id: str=None): - """Returns user for google login - - Checks email of the user to find an existing user. - If found, the user is returned. - Else, a new user is created and returned - - Arguments: - email: email of the user, used to check for existing user - - Returns: - Returns authenticated user - """ - - # If apple auth id given, first try to find existing user using it - if apple_auth_id: - user = UserModel.find_by_apple_auth_id(apple_auth_id) - # if user not found, try finding using email - if not user: - user = UserModel.find_by_email(email) - - # If apple auth id not given, try finding using email - else: - user = UserModel.find_by_email(email) - - if user: - return user - else: - return None - @staticmethod @email_verification_required def get_achievements(user_id: int): diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 08ea8ae83..df56efe0b 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -434,16 +434,18 @@ def post(cls): email = request.json.get("email") # get existing user - user = DAO.get_user_for_social_login(email, apple_auth_id) + user = DAO.get_user_by_email(email) # if user not found, create a new user if not user: data = request.json user = DAO.create_user_using_social_login(data, apple_auth_id) - # else, set apple auth id to later identify the user + # if user found, confirm it is for the same social sign in provider else: - user.apple_auth_id = apple_auth_id - user.save_to_db() + social_sign_in_details = DAO.get_social_sign_in_details(user.id, "apple") + # if details not present, return error + if not social_sign_in_details: + return messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER, HTTPStatus.NOT_FOUND return create_tokens_for_new_user_and_return(user) @@ -472,12 +474,18 @@ def post(cls): idinfo = id_token.verify_oauth2_token(token, requests.Request(), client_id) # id_token is valid. Get user. - user = DAO.get_user_for_social_login(email) - + user = DAO.get_user_by_email(email) + if not user: # create a new user data = request.json user = DAO.create_user_using_social_login(data) + # if user found, confirm it is for the same social sign in provider + else: + social_sign_in_details = DAO.get_social_sign_in_details(user.id, "google") + # if details not present, return error + if not social_sign_in_details: + return messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER, HTTPStatus.NOT_FOUND return create_tokens_for_new_user_and_return(user) diff --git a/app/database/models/social_sign_in.py b/app/database/models/social_sign_in.py new file mode 100644 index 000000000..0e345bb86 --- /dev/null +++ b/app/database/models/social_sign_in.py @@ -0,0 +1,41 @@ +from app.database.models.user import UserModel +from app.database.sqlalchemy_extension import db + +class SocialSignInModel(db.Model): + """Data model representation of social sign in of a user. + + Attributes: + user_id: user_id, to identify the user in user model + social_sign_in_type: social sign in type (google, apple) + id_token: id_token sent by the social sign in provider + associated_email: email of the user associated with the social sign in provider + full_name: full name of the user associated with the social sign in provider + """ + + # Specifying database table used for SocialSignInModel + __tablename__ = "social_sign_in" + __table_args__ = {"extend_existing": True} + + # data properties + user_id = db.Column(db.Integer, db.ForeignKey("users.id")) + social_sign_in_type = db.Column(db.String(20)) + id_token = db.Column(db.String(100), primary_key=True) + associated_email = db.Column(db.String(50)) + full_name = db.Column(db.String(50)) + + def __init__(self, user_id, sign_in_type, id_token, email, name): + self.user_id = user_id + self.social_sign_in_type = sign_in_type + self.id_token = id_token + self.associated_email = email + self.full_name = name + + @classmethod + def get_social_sign_in_details(cls, user_id: int, social_sign_in_type: str): + """Returns social sign in details of the user for the specified type""" + return cls.query.filter_by(user_id=user_id, social_sign_in_type=social_sign_in_type).first() + + def save_to_db(self): + """Adds the social sign in details to the database.""" + db.session.add(self) + db.session.commit() \ No newline at end of file diff --git a/app/database/models/user.py b/app/database/models/user.py index 00c0004fd..ea7cd837c 100644 --- a/app/database/models/user.py +++ b/app/database/models/user.py @@ -29,7 +29,6 @@ class UserModel(db.Model): # security password_hash = db.Column(db.String(100)) - apple_auth_id = db.Column(db.String(100)) # registration registration_date = db.Column(db.Float) @@ -60,7 +59,7 @@ class UserModel(db.Model): need_mentoring = db.Column(db.Boolean) available_to_mentor = db.Column(db.Boolean) - def __init__(self, name, username, password, email, terms_and_conditions_checked, social_login=False, apple_auth_id=None): + def __init__(self, name, username, password, email, terms_and_conditions_checked, social_login=False): """Initialises userModel class with name, username, password, email, and terms_and_conditions_checked. """ ## required fields @@ -73,10 +72,6 @@ def __init__(self, name, username, password, email, terms_and_conditions_checked if not social_login: self.set_password(password) - # save auth id for sign in with apple (if present) - if apple_auth_id: - self.apple_auth_id = apple_auth_id - # default values self.is_admin = True if self.is_empty() else False # first user is admin if social_login: @@ -138,11 +133,6 @@ def find_by_id(cls, _id: int) -> 'UserModel': """Returns the user that has the id we searched for. """ return cls.query.filter_by(id=_id).first() - @classmethod - def find_by_apple_auth_id(cls, _id: str) -> 'UserModel': - """Returns the user that has apple auth id we searched for.""" - return cls.query.filter_by(apple_auth_id=_id).first() - @classmethod def get_all_admins(cls, is_admin=True): """Returns all the admins. """ diff --git a/app/messages.py b/app/messages.py index 6df51d72a..ae8b13d0d 100644 --- a/app/messages.py +++ b/app/messages.py @@ -206,6 +206,7 @@ "message": "Check your email, a new verification" " email was sent." } GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED = "Google auth token verification failed" +USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER = "An account for this email is already present with a different login method." # Success messages TASK_WAS_ALREADY_ACHIEVED = {"message": "Task was already achieved."} From ba8528db276187be46376b0bc8283d98a266b3c7 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Fri, 14 Aug 2020 02:38:06 +0530 Subject: [PATCH 07/20] social sign in implementation finalized --- app/api/dao/user.py | 9 ++++++--- app/api/resources/user.py | 5 ++++- app/database/models/social_sign_in.py | 5 +++++ app/messages.py | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/api/dao/user.py b/app/api/dao/user.py index 0ff1acd7e..21f74501f 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -82,6 +82,7 @@ def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None The new user created """ + id_token = data["id_token"] name = data["name"] username = None password = None @@ -89,15 +90,17 @@ def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None terms_and_conditions_checked = True social_login = True + # Check if user for this id_token already exists + if SocialSignInModel.find_by_id_token(id_token): + return messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS, HTTPStatus.BAD_REQUEST + # create and save user user = UserModel(name, username, password, email, terms_and_conditions_checked, social_login) user.save_to_db() # create and save social sign in details for the user - user_id = UserModel.find_by_email(email).id social_sign_in_type = "apple" if apple_auth_id else "google" - id_token = data["id_token"] - social_sign_in_details = SocialSignInModel(user_id, social_sign_in_type, id_token, email, name) + social_sign_in_details = SocialSignInModel(user.id, social_sign_in_type, id_token, email, name) social_sign_in_details.save_to_db() return user diff --git a/app/api/resources/user.py b/app/api/resources/user.py index df56efe0b..e38cd6624 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -440,6 +440,9 @@ def post(cls): if not user: data = request.json user = DAO.create_user_using_social_login(data, apple_auth_id) + # If any error occured, return error + if user[1] == HTTPStatus.BAD_REQUEST: + return user # if user found, confirm it is for the same social sign in provider else: social_sign_in_details = DAO.get_social_sign_in_details(user.id, "apple") @@ -475,7 +478,7 @@ def post(cls): # id_token is valid. Get user. user = DAO.get_user_by_email(email) - + if not user: # create a new user data = request.json diff --git a/app/database/models/social_sign_in.py b/app/database/models/social_sign_in.py index 0e345bb86..e31db2fd5 100644 --- a/app/database/models/social_sign_in.py +++ b/app/database/models/social_sign_in.py @@ -35,6 +35,11 @@ def get_social_sign_in_details(cls, user_id: int, social_sign_in_type: str): """Returns social sign in details of the user for the specified type""" return cls.query.filter_by(user_id=user_id, social_sign_in_type=social_sign_in_type).first() + @classmethod + def find_by_id_token(cls, id_token: str): + """Finds user using id_token""" + return cls.query.filter_by(id_token=id_token).first() + def save_to_db(self): """Adds the social sign in details to the database.""" db.session.add(self) diff --git a/app/messages.py b/app/messages.py index ae8b13d0d..2a28e295f 100644 --- a/app/messages.py +++ b/app/messages.py @@ -207,6 +207,7 @@ } GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED = "Google auth token verification failed" USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER = "An account for this email is already present with a different login method." +ANOTHER_USER_FOR_ID_TOKEN_EXISTS = "User for this id token already exists. A new account can't be created." # Success messages TASK_WAS_ALREADY_ACHIEVED = {"message": "Task was already achieved."} From c6b1aea01db76c7c0f4aa1425db22370f79706ac Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Fri, 14 Aug 2020 02:47:31 +0530 Subject: [PATCH 08/20] code documentation and logic improved --- app/api/dao/user.py | 4 ++-- app/api/models/user.py | 2 +- app/api/resources/user.py | 4 ++-- app/database/models/social_sign_in.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/api/dao/user.py b/app/api/dao/user.py index 21f74501f..f31dfa77f 100644 --- a/app/api/dao/user.py +++ b/app/api/dao/user.py @@ -71,12 +71,13 @@ def create_user(data: Dict[str, str]): return messages.USER_WAS_CREATED_SUCCESSFULLY, HTTPStatus.CREATED @staticmethod - def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None): + def create_user_using_social_login(data: Dict[str, str], social_sign_in_type: str): """ Creates a new user using Google Auth. Arguments: data: A list containing the user's name and email + social_sign_in_type: social sign in provider (apple, google) Returns: The new user created @@ -99,7 +100,6 @@ def create_user_using_social_login(data: Dict[str, str], apple_auth_id: str=None user.save_to_db() # create and save social sign in details for the user - social_sign_in_type = "apple" if apple_auth_id else "google" social_sign_in_details = SocialSignInModel(user.id, social_sign_in_type, id_token, email, name) social_sign_in_details.save_to_db() diff --git a/app/api/models/user.py b/app/api/models/user.py index 785807ad4..272ce1b79 100644 --- a/app/api/models/user.py +++ b/app/api/models/user.py @@ -154,7 +154,7 @@ def add_models_to_namespace(api_namespace): ) social_auth_body_model = Model( - "Google authentication data model", + "Social sign-in authentication data model", { "id_token": fields.String(required=True, description="User's idToken given by Google auth"), "name": fields.String(required=True, description="User's name given by Google auth"), diff --git a/app/api/resources/user.py b/app/api/resources/user.py index e38cd6624..6ed2e266d 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -439,7 +439,7 @@ def post(cls): # if user not found, create a new user if not user: data = request.json - user = DAO.create_user_using_social_login(data, apple_auth_id) + user = DAO.create_user_using_social_login(data, "apple") # If any error occured, return error if user[1] == HTTPStatus.BAD_REQUEST: return user @@ -482,7 +482,7 @@ def post(cls): if not user: # create a new user data = request.json - user = DAO.create_user_using_social_login(data) + user = DAO.create_user_using_social_login(data, "google") # if user found, confirm it is for the same social sign in provider else: social_sign_in_details = DAO.get_social_sign_in_details(user.id, "google") diff --git a/app/database/models/social_sign_in.py b/app/database/models/social_sign_in.py index e31db2fd5..0a67de868 100644 --- a/app/database/models/social_sign_in.py +++ b/app/database/models/social_sign_in.py @@ -6,7 +6,7 @@ class SocialSignInModel(db.Model): Attributes: user_id: user_id, to identify the user in user model - social_sign_in_type: social sign in type (google, apple) + social_sign_in_type: social sign in type (apple, google) id_token: id_token sent by the social sign in provider associated_email: email of the user associated with the social sign in provider full_name: full name of the user associated with the social sign in provider From 470d572cba455ad65fdd71498368b197f2479b9c Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Fri, 14 Aug 2020 04:55:23 +0530 Subject: [PATCH 09/20] messages cosntants fixed. API code refactored and re-used. --- app/api/resources/user.py | 71 ++++++++++++++++++++------------------- app/messages.py | 6 ++-- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 6ed2e266d..83190aa20 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -48,6 +48,26 @@ def create_tokens_for_new_user_and_return(user): HTTPStatus.OK, ) +def perform_social_sign_in_and_return_response(email:str, provider: str): + # get existing user + user = DAO.get_user_by_email(email) + + # if user not found, create a new user + if not user: + data = request.json + user = DAO.create_user_using_social_login(data, provider) + # If any error occured, return error + if user[1] == HTTPStatus.BAD_REQUEST: + return user + # if user found, confirm it is for the same social sign in provider + else: + social_sign_in_details = DAO.get_social_sign_in_details(user.id, provider) + # if details not present, return error + if not social_sign_in_details: + return messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER, HTTPStatus.NOT_FOUND + + return create_tokens_for_new_user_and_return(user) + @users_ns.route("users") @users_ns.response( HTTPStatus.UNAUTHORIZED, @@ -420,6 +440,12 @@ class AppleAuth(Resource): @classmethod @users_ns.doc("apple-auth callback") @users_ns.response(HTTPStatus.OK, "Successful login", login_response_body_model) + @users_ns.doc( + responses={ + HTTPStatus.BAD_REQUEST: f"{messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS}", + HTTPStatus.NOT_FOUND: f"{messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER}" + } + ) @users_ns.expect(social_auth_body_model) def post(cls): """ @@ -430,34 +456,22 @@ def post(cls): If email found, that account is used. Else, a new account is created. """ - apple_auth_id = request.json.get("id_token") email = request.json.get("email") - - # get existing user - user = DAO.get_user_by_email(email) - # if user not found, create a new user - if not user: - data = request.json - user = DAO.create_user_using_social_login(data, "apple") - # If any error occured, return error - if user[1] == HTTPStatus.BAD_REQUEST: - return user - # if user found, confirm it is for the same social sign in provider - else: - social_sign_in_details = DAO.get_social_sign_in_details(user.id, "apple") - # if details not present, return error - if not social_sign_in_details: - return messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER, HTTPStatus.NOT_FOUND - - return create_tokens_for_new_user_and_return(user) + return perform_social_sign_in_and_return_response(email, "apple") @users_ns.route("google/auth/callback") class GoogleAuth(Resource): @classmethod @users_ns.doc("google-auth callback") @users_ns.response(HTTPStatus.OK, "Successful login", login_response_body_model) - @users_ns.response(HTTPStatus.UNAUTHORIZED, f"{messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED}") + @users_ns.doc( + responses={ + HTTPStatus.UNAUTHORIZED: f"{messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED}", + HTTPStatus.BAD_REQUEST: f"{messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS}", + HTTPStatus.NOT_FOUND: f"{messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER}" + } + ) @users_ns.expect(social_auth_body_model) def post(cls): """ @@ -476,21 +490,8 @@ def post(cls): try: idinfo = id_token.verify_oauth2_token(token, requests.Request(), client_id) - # id_token is valid. Get user. - user = DAO.get_user_by_email(email) - - if not user: - # create a new user - data = request.json - user = DAO.create_user_using_social_login(data, "google") - # if user found, confirm it is for the same social sign in provider - else: - social_sign_in_details = DAO.get_social_sign_in_details(user.id, "google") - # if details not present, return error - if not social_sign_in_details: - return messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER, HTTPStatus.NOT_FOUND - - return create_tokens_for_new_user_and_return(user) + # id_token is valid. Perform social sign in. + return perform_social_sign_in_and_return_response(email, "google") except ValueError: return messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED, HTTPStatus.UNAUTHORIZED diff --git a/app/messages.py b/app/messages.py index 2a28e295f..b10c1573e 100644 --- a/app/messages.py +++ b/app/messages.py @@ -205,9 +205,9 @@ EMAIL_VERIFICATION_MESSAGE = { "message": "Check your email, a new verification" " email was sent." } -GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED = "Google auth token verification failed" -USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER = "An account for this email is already present with a different login method." -ANOTHER_USER_FOR_ID_TOKEN_EXISTS = "User for this id token already exists. A new account can't be created." +GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED = {"message": "Google auth token verification failed"} +USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER = {"message": "An account for this email is already present with a different login method."} +ANOTHER_USER_FOR_ID_TOKEN_EXISTS = {"message": "User for this id token already exists. A new account can't be created."} # Success messages TASK_WAS_ALREADY_ACHIEVED = {"message": "Task was already achieved."} From f78e925c8e8bc5268d5c41279dfa9a6d29dc6ff3 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Sat, 15 Aug 2020 03:14:10 +0530 Subject: [PATCH 10/20] tests added and logic improved --- app/api/resources/user.py | 3 +- tests/users/test_api_social_sign_in.py | 98 ++++++++++++++++++++++++++ tests/users/test_dao_social_sign_in.py | 33 +++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/users/test_api_social_sign_in.py create mode 100644 tests/users/test_dao_social_sign_in.py diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 83190aa20..716a79ae9 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -15,6 +15,7 @@ from app.api.email_utils import send_email_verification_message from app.api.models.user import * from app.api.dao.user import UserDAO +from app.database.models.user import UserModel from app.api.resources.common import auth_header_parser from google.oauth2 import id_token from google.auth.transport import requests @@ -57,7 +58,7 @@ def perform_social_sign_in_and_return_response(email:str, provider: str): data = request.json user = DAO.create_user_using_social_login(data, provider) # If any error occured, return error - if user[1] == HTTPStatus.BAD_REQUEST: + if not isinstance(user, UserModel): return user # if user found, confirm it is for the same social sign in provider else: diff --git a/tests/users/test_api_social_sign_in.py b/tests/users/test_api_social_sign_in.py new file mode 100644 index 000000000..9b7943e6d --- /dev/null +++ b/tests/users/test_api_social_sign_in.py @@ -0,0 +1,98 @@ +import unittest + +from flask import json + +from app import messages +from app.database.sqlalchemy_extension import db +from tests.base_test_case import BaseTestCase + +from app.database.models.user import UserModel +from app.api.dao.user import UserDAO +from http import HTTPStatus + +class TestSocialSignInAPI(BaseTestCase): + def test_create_new_user_for_social_sign_in(self): + with self.client: + response = self.client.post( + "/apple/auth/callback", + data=json.dumps( + dict(id_token="test_token", name="test_name", email="test_email") + ), + follow_redirects=True, + content_type="application/json", + ) + self.assertIsNotNone(response.json.get("access_token")) + self.assertIsNotNone(response.json.get("access_expiry")) + self.assertIsNotNone(response.json.get("refresh_token")) + self.assertIsNotNone(response.json.get("refresh_expiry")) + + def test_another_user_for_id_token_exists(self): + # Create user (token = test_token) + user_data = dict( + id_token="test_token", + name="test_name", + email="test_email" + ) + social_sign_in_type = "test_type" + UserDAO.create_user_using_social_login(user_data, social_sign_in_type) + + # Use API. Keep token same, other information different + with self.client: + response = self.client.post( + "/apple/auth/callback", + data=json.dumps( + dict(id_token="test_token", name="test_name_2", email="test_email_2") + ), + follow_redirects=True, + content_type="application/json", + ) + self.assertIsNone(response.json.get("access_token")) + self.assertIsNone(response.json.get("access_expiry")) + self.assertIsNone(response.json.get("refresh_token")) + self.assertIsNone(response.json.get("refresh_expiry")) + self.assertTrue({"message": response.json.get("message")} == messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS) + + def test_user_email_signed_in_with_different_provider(self): + # Create user (email = test_email) + user_data = dict( + id_token="test_token", + name="test_name", + email="test_email" + ) + social_sign_in_type = "google" + UserDAO.create_user_using_social_login(user_data, social_sign_in_type) + + # Use API. Keep email same, provider different (apple) + with self.client: + response = self.client.post( + "/apple/auth/callback", + data=json.dumps( + dict(id_token="test_token_2", name="test_name_2", email="test_email") + ), + follow_redirects=True, + content_type="application/json", + ) + self.assertIsNone(response.json.get("access_token")) + self.assertIsNone(response.json.get("access_expiry")) + self.assertIsNone(response.json.get("refresh_token")) + self.assertIsNone(response.json.get("refresh_expiry")) + self.assertTrue({"message": response.json.get("message")} == messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER) + + def test_google_auth_token_not_verified(self): + with self.client: + response = self.client.post( + "/google/auth/callback", + data=json.dumps( + dict(id_token="invalid_token", name="test_name", email="test_email") + ), + follow_redirects=True, + content_type="application/json", + ) + self.assertIsNone(response.json.get("access_token")) + self.assertIsNone(response.json.get("access_expiry")) + self.assertIsNone(response.json.get("refresh_token")) + self.assertIsNone(response.json.get("refresh_expiry")) + self.assertTrue({"message": response.json.get("message")} == messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/users/test_dao_social_sign_in.py b/tests/users/test_dao_social_sign_in.py new file mode 100644 index 000000000..7141f0bc7 --- /dev/null +++ b/tests/users/test_dao_social_sign_in.py @@ -0,0 +1,33 @@ +import unittest + +from app.api.dao.user import UserDAO +from tests.base_test_case import BaseTestCase +from app.database.models.user import UserModel + + +class TestSocialSignIn(BaseTestCase): + def test_create_user_using_social_sign_in(self): + user_data = dict( + id_token="test_token", + name="test_name", + email="test_email" + ) + social_sign_in_type = "test_type" + + # Call create user using social sign in method + UserDAO.create_user_using_social_login(user_data, social_sign_in_type) + + # Test the user created + user = UserDAO.get_user_by_email("test_email") + self.assertTrue(user.name == user_data["name"]) + self.assertTrue(user.username is None) + self.assertTrue(user.password_hash is None) + + # Test the social sign in details of the user created + social_sign_in_details = UserDAO.get_social_sign_in_details(user.id, "test_type") + self.assertTrue(social_sign_in_details.id_token == user_data["id_token"]) + self.assertTrue(social_sign_in_details.associated_email == user_data["email"]) + self.assertTrue(social_sign_in_details.full_name == user_data["name"]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 29708e3691b43eae755da968e2c1bead3cb375f2 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Sat, 15 Aug 2020 03:39:39 +0530 Subject: [PATCH 11/20] social sign in documentation added in user_authentication.md --- docs/user_authentication.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index 30998c51b..6d2370fe0 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -13,3 +13,20 @@ The user can then use this `access_token` when using a protected/restricted API, Here's an inside look at an `access_token` using [jwt.io](https://jwt.io) Debugger. ![image](https://user-images.githubusercontent.com/11148726/44627573-1de2f800-a928-11e8-87a7-0107b0a622bc.png) + +## Social Sign In + +In addition to authenticating a user using username and password using the (`POST /login`) API, convenient social sign-in using Apple and Google sign-in is also there. This is done using the (`POST /apple/auth/callback`) and (`POST /google/auth/callback`) APIs. + +All the three APIs return the same data model for a successful login, making the implementation in the client app simpler. The flow of social sign-in starts with the client app, where the user uses a provider (Apple or Google) to sign-in with. The provider authenticates the user on their end and send a user unique id_token, full_name, and email. This data is then used in the callback APIs to authenticate the user on the Mentorship System backend. + +To enable the social-sign in functionality, a separate social sign in table has been created which links with the users table using the user id and stores the social sign in data such as id_token, associated_email, etc. The functionality has been designed in a way where linking of different accounts can be enabled as a future scope. + +The callback APIs work as follows: +1. The email of the user is used to find an existing user in the database. +2. If a user is not found for the email, a new user is created using the data. If a user with the unique id_token already exists on the system, an error message is returned. Else, tokens are generated and returned, succesfully signing in the user. +3. If a user is found, the social sign in details are verified for the user id and the exact provider (apple/google). If social sign in record is not found, an error is returned. Else, tokens are generated an returned, succesfully signing in the user. + +Official Developer Documentation: +[Sign In with Apple](https://developer.apple.com/sign-in-with-apple/get-started/) +[Sign In with Google](https://developers.google.com/identity/sign-in/ios/start?ver=swift) \ No newline at end of file From a68ee3922a961fb73374e71779b72e0b3841fe94 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Tue, 18 Aug 2020 14:21:47 +0530 Subject: [PATCH 12/20] code documentation improved --- app/api/resources/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 716a79ae9..dbb6ed0de 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -59,6 +59,7 @@ def perform_social_sign_in_and_return_response(email:str, provider: str): user = DAO.create_user_using_social_login(data, provider) # If any error occured, return error if not isinstance(user, UserModel): + # If variable user is not of type UserModel, then that means it contains the error message and the HTTP code. return user # if user found, confirm it is for the same social sign in provider else: From bb8c5583bfa2e8920a73e439f5aeb410f91b14f9 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Wed, 19 Aug 2020 12:26:19 +0530 Subject: [PATCH 13/20] error unwrapping done to make code clearer. google auth client id made constant in config.py --- app/api/resources/user.py | 8 +++++--- config.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/api/resources/user.py b/app/api/resources/user.py index dbb6ed0de..85d4da224 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -19,6 +19,7 @@ from app.api.resources.common import auth_header_parser from google.oauth2 import id_token from google.auth.transport import requests +from config import GOOGLE_AUTH_CLIENT_ID users_ns = Namespace("Users", description="Operations related to users") add_models_to_namespace(users_ns) @@ -60,7 +61,9 @@ def perform_social_sign_in_and_return_response(email:str, provider: str): # If any error occured, return error if not isinstance(user, UserModel): # If variable user is not of type UserModel, then that means it contains the error message and the HTTP code. - return user + error_message = user[0] + http_code = user[1] + return error_message, http_code # if user found, confirm it is for the same social sign in provider else: social_sign_in_details = DAO.get_social_sign_in_details(user.id, provider) @@ -486,11 +489,10 @@ def post(cls): token = request.json.get("id_token") email = request.json.get("email") - client_id = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com" # Verify google auth id token try: - idinfo = id_token.verify_oauth2_token(token, requests.Request(), client_id) + idinfo = id_token.verify_oauth2_token(token, requests.Request(), GOOGLE_AUTH_CLIENT_ID) # id_token is valid. Perform social sign in. return perform_social_sign_in_and_return_response(email, "google") diff --git a/config.py b/config.py index 90f254e55..34a6dc43c 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ import os from datetime import timedelta +GOOGLE_AUTH_CLIENT_ID = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com" def get_mock_email_config() -> bool: MOCK_EMAIL = os.getenv("MOCK_EMAIL") From a9fc3d9ac304394a0770f86921ff68619961ac0c Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Thu, 20 Aug 2020 01:16:59 +0530 Subject: [PATCH 14/20] google auth client id used from .env now --- .env.template | 1 + app/api/resources/user.py | 4 ++-- config.py | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.env.template b/.env.template index b042d68ac..5690b8bff 100644 --- a/.env.template +++ b/.env.template @@ -6,3 +6,4 @@ MAIL_SERVER = APP_MAIL_USERNAME = APP_MAIL_PASSWORD = MOCK_EMAIL = +GOOGLE_AUTH_CLIENT_ID = \ No newline at end of file diff --git a/app/api/resources/user.py b/app/api/resources/user.py index 85d4da224..fb55e9c7a 100644 --- a/app/api/resources/user.py +++ b/app/api/resources/user.py @@ -19,7 +19,7 @@ from app.api.resources.common import auth_header_parser from google.oauth2 import id_token from google.auth.transport import requests -from config import GOOGLE_AUTH_CLIENT_ID +import os users_ns = Namespace("Users", description="Operations related to users") add_models_to_namespace(users_ns) @@ -492,7 +492,7 @@ def post(cls): # Verify google auth id token try: - idinfo = id_token.verify_oauth2_token(token, requests.Request(), GOOGLE_AUTH_CLIENT_ID) + idinfo = id_token.verify_oauth2_token(token, requests.Request(), os.getenv("GOOGLE_AUTH_CLIENT_ID")) # id_token is valid. Perform social sign in. return perform_social_sign_in_and_return_response(email, "google") diff --git a/config.py b/config.py index 34a6dc43c..d9b54fd7d 100644 --- a/config.py +++ b/config.py @@ -1,8 +1,6 @@ import os from datetime import timedelta -GOOGLE_AUTH_CLIENT_ID = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com" - def get_mock_email_config() -> bool: MOCK_EMAIL = os.getenv("MOCK_EMAIL") From e488923c54db422a1bc1f86b7fbdbfd86d9ff975 Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Fri, 21 Aug 2020 11:55:21 +0530 Subject: [PATCH 15/20] unnecessary if removed --- app/database/models/user.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/database/models/user.py b/app/database/models/user.py index ea7cd837c..852158545 100644 --- a/app/database/models/user.py +++ b/app/database/models/user.py @@ -74,10 +74,8 @@ def __init__(self, name, username, password, email, terms_and_conditions_checked # default values self.is_admin = True if self.is_empty() else False # first user is admin - if social_login: - self.is_email_verified = True - else: - self.is_email_verified = False + # email is verified (True) for social login, False for normal login + self.is_email_verified = social_login self.registration_date = time.time() ## optional fields From 191f51fe540c2091cdcbb602374089ecd12d39ee Mon Sep 17 00:00:00 2001 From: Yugantar Jain <31164725+yugantarjain@users.noreply.github.com> Date: Mon, 24 Aug 2020 10:14:45 +0530 Subject: [PATCH 16/20] Update docs/user_authentication.md Co-authored-by: Isabel Costa <11148726+isabelcosta@users.noreply.github.com> --- docs/user_authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index 6d2370fe0..be972d479 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -18,7 +18,7 @@ Here's an inside look at an `access_token` using [jwt.io](https://jwt.io) Debugg In addition to authenticating a user using username and password using the (`POST /login`) API, convenient social sign-in using Apple and Google sign-in is also there. This is done using the (`POST /apple/auth/callback`) and (`POST /google/auth/callback`) APIs. -All the three APIs return the same data model for a successful login, making the implementation in the client app simpler. The flow of social sign-in starts with the client app, where the user uses a provider (Apple or Google) to sign-in with. The provider authenticates the user on their end and send a user unique id_token, full_name, and email. This data is then used in the callback APIs to authenticate the user on the Mentorship System backend. +All the three APIs return the same data model for a successful login, making the implementation in the client app simpler. The flow of social sign-in starts with the client app, where the user uses a provider (Apple or Google) to sign-in with. The provider authenticates the user on their end and sends a user unique `id_token`, `full_name`, and `email`. This data is then used in the callback APIs to authenticate the user on the Mentorship System backend. To enable the social-sign in functionality, a separate social sign in table has been created which links with the users table using the user id and stores the social sign in data such as id_token, associated_email, etc. The functionality has been designed in a way where linking of different accounts can be enabled as a future scope. @@ -29,4 +29,4 @@ The callback APIs work as follows: Official Developer Documentation: [Sign In with Apple](https://developer.apple.com/sign-in-with-apple/get-started/) -[Sign In with Google](https://developers.google.com/identity/sign-in/ios/start?ver=swift) \ No newline at end of file +[Sign In with Google](https://developers.google.com/identity/sign-in/ios/start?ver=swift) From 665fb6cc7bc440060772c4d8a3068d3acfa6a55e Mon Sep 17 00:00:00 2001 From: Yugantar Jain <31164725+yugantarjain@users.noreply.github.com> Date: Mon, 24 Aug 2020 10:15:02 +0530 Subject: [PATCH 17/20] Update docs/user_authentication.md Co-authored-by: Isabel Costa <11148726+isabelcosta@users.noreply.github.com> --- docs/user_authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index be972d479..5a50d95de 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -20,7 +20,7 @@ In addition to authenticating a user using username and password using the (`POS All the three APIs return the same data model for a successful login, making the implementation in the client app simpler. The flow of social sign-in starts with the client app, where the user uses a provider (Apple or Google) to sign-in with. The provider authenticates the user on their end and sends a user unique `id_token`, `full_name`, and `email`. This data is then used in the callback APIs to authenticate the user on the Mentorship System backend. -To enable the social-sign in functionality, a separate social sign in table has been created which links with the users table using the user id and stores the social sign in data such as id_token, associated_email, etc. The functionality has been designed in a way where linking of different accounts can be enabled as a future scope. +To enable the social sign-in functionality, a separate social sign-in table has been created which links with the `users` table using the user id and stores the social sign-in data such as id_token, associated_email, etc. The functionality has been designed in a way where linking of different accounts can be enabled as a future scope. The callback APIs work as follows: 1. The email of the user is used to find an existing user in the database. From a0eda7b48874d9a0891372092bc8320193d8ae5b Mon Sep 17 00:00:00 2001 From: Yugantar Jain <31164725+yugantarjain@users.noreply.github.com> Date: Mon, 24 Aug 2020 10:15:28 +0530 Subject: [PATCH 18/20] Update docs/user_authentication.md Co-authored-by: Isabel Costa <11148726+isabelcosta@users.noreply.github.com> --- docs/user_authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index 5a50d95de..ad88a321d 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -24,7 +24,7 @@ To enable the social sign-in functionality, a separate social sign-in table has The callback APIs work as follows: 1. The email of the user is used to find an existing user in the database. -2. If a user is not found for the email, a new user is created using the data. If a user with the unique id_token already exists on the system, an error message is returned. Else, tokens are generated and returned, succesfully signing in the user. +2. If a user is not found for the email, a new user is created using the data. If a user with the unique `id_token` already exists on the system, an error message is returned. Else, tokens are generated and returned, successfully signing in the user. 3. If a user is found, the social sign in details are verified for the user id and the exact provider (apple/google). If social sign in record is not found, an error is returned. Else, tokens are generated an returned, succesfully signing in the user. Official Developer Documentation: From db33773cce3612f4b1fb206904327a3403a1f4f4 Mon Sep 17 00:00:00 2001 From: Yugantar Jain <31164725+yugantarjain@users.noreply.github.com> Date: Mon, 24 Aug 2020 10:15:45 +0530 Subject: [PATCH 19/20] Update docs/user_authentication.md Co-authored-by: Isabel Costa <11148726+isabelcosta@users.noreply.github.com> --- docs/user_authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index ad88a321d..01df51450 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -25,7 +25,7 @@ To enable the social sign-in functionality, a separate social sign-in table has The callback APIs work as follows: 1. The email of the user is used to find an existing user in the database. 2. If a user is not found for the email, a new user is created using the data. If a user with the unique `id_token` already exists on the system, an error message is returned. Else, tokens are generated and returned, successfully signing in the user. -3. If a user is found, the social sign in details are verified for the user id and the exact provider (apple/google). If social sign in record is not found, an error is returned. Else, tokens are generated an returned, succesfully signing in the user. +3. If a user is found, the social sign-in details are verified for the user id and the exact provider (apple/google). If a social sign-in record is not found, an error is returned. Else, tokens are generated an returned, successfully signing in the user. Official Developer Documentation: [Sign In with Apple](https://developer.apple.com/sign-in-with-apple/get-started/) From d6d4aa3374de5e25b954f308db81df7f3641438f Mon Sep 17 00:00:00 2001 From: Yugantar Jain Date: Mon, 24 Aug 2020 10:24:51 +0530 Subject: [PATCH 20/20] assertEqual used in tests. doc improved. --- docs/user_authentication.md | 4 ++-- tests/users/test_api_social_sign_in.py | 6 +++--- tests/users/test_dao_social_sign_in.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/user_authentication.md b/docs/user_authentication.md index 01df51450..e90938941 100644 --- a/docs/user_authentication.md +++ b/docs/user_authentication.md @@ -28,5 +28,5 @@ The callback APIs work as follows: 3. If a user is found, the social sign-in details are verified for the user id and the exact provider (apple/google). If a social sign-in record is not found, an error is returned. Else, tokens are generated an returned, successfully signing in the user. Official Developer Documentation: -[Sign In with Apple](https://developer.apple.com/sign-in-with-apple/get-started/) -[Sign In with Google](https://developers.google.com/identity/sign-in/ios/start?ver=swift) +* [Sign In with Apple](https://developer.apple.com/sign-in-with-apple/get-started/) +* [Sign In with Google](https://developers.google.com/identity/sign-in/ios/start?ver=swift) diff --git a/tests/users/test_api_social_sign_in.py b/tests/users/test_api_social_sign_in.py index 9b7943e6d..5fe169676 100644 --- a/tests/users/test_api_social_sign_in.py +++ b/tests/users/test_api_social_sign_in.py @@ -50,7 +50,7 @@ def test_another_user_for_id_token_exists(self): self.assertIsNone(response.json.get("access_expiry")) self.assertIsNone(response.json.get("refresh_token")) self.assertIsNone(response.json.get("refresh_expiry")) - self.assertTrue({"message": response.json.get("message")} == messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS) + self.assertEqual({"message": response.json.get("message")}, messages.ANOTHER_USER_FOR_ID_TOKEN_EXISTS) def test_user_email_signed_in_with_different_provider(self): # Create user (email = test_email) @@ -76,7 +76,7 @@ def test_user_email_signed_in_with_different_provider(self): self.assertIsNone(response.json.get("access_expiry")) self.assertIsNone(response.json.get("refresh_token")) self.assertIsNone(response.json.get("refresh_expiry")) - self.assertTrue({"message": response.json.get("message")} == messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER) + self.assertEqual({"message": response.json.get("message")}, messages.USER_NOT_SIGNED_IN_WITH_THIS_PROVIDER) def test_google_auth_token_not_verified(self): with self.client: @@ -92,7 +92,7 @@ def test_google_auth_token_not_verified(self): self.assertIsNone(response.json.get("access_expiry")) self.assertIsNone(response.json.get("refresh_token")) self.assertIsNone(response.json.get("refresh_expiry")) - self.assertTrue({"message": response.json.get("message")} == messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED) + self.assertEqual({"message": response.json.get("message")}, messages.GOOGLE_AUTH_TOKEN_VERIFICATION_FAILED) if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/tests/users/test_dao_social_sign_in.py b/tests/users/test_dao_social_sign_in.py index 7141f0bc7..0ad7837e8 100644 --- a/tests/users/test_dao_social_sign_in.py +++ b/tests/users/test_dao_social_sign_in.py @@ -19,15 +19,15 @@ def test_create_user_using_social_sign_in(self): # Test the user created user = UserDAO.get_user_by_email("test_email") - self.assertTrue(user.name == user_data["name"]) + self.assertEqual(user.name, user_data["name"]) self.assertTrue(user.username is None) self.assertTrue(user.password_hash is None) # Test the social sign in details of the user created social_sign_in_details = UserDAO.get_social_sign_in_details(user.id, "test_type") - self.assertTrue(social_sign_in_details.id_token == user_data["id_token"]) - self.assertTrue(social_sign_in_details.associated_email == user_data["email"]) - self.assertTrue(social_sign_in_details.full_name == user_data["name"]) + self.assertEqual(social_sign_in_details.id_token, user_data["id_token"]) + self.assertEqual(social_sign_in_details.associated_email, user_data["email"]) + self.assertEqual(social_sign_in_details.full_name, user_data["name"]) if __name__ == "__main__": unittest.main() \ No newline at end of file