-
Notifications
You must be signed in to change notification settings - Fork 447
feat: Social Logins #719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Social Logins #719
Changes from 4 commits
a774cc8
fb6408f
9b8cedf
4cea797
6340c57
dd9216a
ba8528d
c6b1aea
470d572
f78e925
29708e3
a68ee39
bb8c558
a9fc3d9
e488923
191f51f
665fb6c
a0eda7b
db33773
d6d4aa3
ddc7a54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -390,6 +392,118 @@ def post(cls): | |
) | ||
|
||
|
||
@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, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can go to a private method in this file or somewhere else, as long as you reuse this logic since you use it in at least 2 different places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
@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.expect(social_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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a secret variable? can you please put this in a constant, either here or probably better in Config.py There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. I checked the config.py file and am not sure how exactly to add this constant there... Should I just do it the way it is done in messages.py? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yugantarjain Read the 4th Point here in Setup and Run part of Docs https://github.com/anitab-org/mentorship-backend. Config.py reads the os.env variables for config. As part of setup of this app, someone sets the env variables. So you just need to add the variable and read from there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Made a constant in config.py. |
||
|
||
# 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_social_login(email) | ||
|
||
if not user: | ||
# create a new user | ||
data = request.json | ||
user = DAO.create_user_using_social_login(data) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will the logic here work fine if this is not a gmail email? I never did anything like this so I am really curious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it will work fine. In-fact, when I tested with Sign in with Apple, I hid my email (the service then returns a private relay email) and it worked perfectly. |
||
# 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): | ||
@classmethod | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ class UserModel(db.Model): | |
|
||
# security | ||
password_hash = db.Column(db.String(100)) | ||
apple_auth_id = db.Column(db.String(100)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for this to work I think you need to create a migration script to add this field to dev database. Can you do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I did create a migration script and ran it locally. Do I need to perform any additional steps? |
||
|
||
# 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): | ||
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 | ||
|
||
|
@@ -69,11 +70,19 @@ 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) | ||
|
||
# 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 | ||
self.is_email_verified = False | ||
if social_login: | ||
self.is_email_verified = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not verify here? Any special usecase? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. For normal login, the email is not verified and the user specifically has to do that from a link they receive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got you, I was thinking something different, not applicable here though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated this to - |
||
else: | ||
self.is_email_verified = False | ||
self.registration_date = time.time() | ||
|
||
## optional fields | ||
|
@@ -129,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. """ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to change? as far as I see you did not change anything to the POST /register API, you created new endpoints that follow a different request model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Actually, when a user signs up using social login, we first try to find them in our database. If a pre-existing user is not found, we create a new one. To facilitate the creation of a new user, these fields were made optional. I did some testing and it works fine (because we have validation checks for them in any case) and all the unit tests passed.
If there is a better solution to this, please let me know. We also have an ongoing discussion about this on zulip mentorship ios channel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good @yugantarjain thank you for your explanation!