Skip to content

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

Closed
wants to merge 21 commits into from

Conversation

yugantarjain
Copy link

@yugantarjain yugantarjain commented Aug 10, 2020

Description

Currently a work in progress, this PR implements social Login options (Google and Apple). This has been done by creating callback APIs for both Google auth and Apple auth.

Flow:

  1. The client uses the social sign in service on the app and receive id_token, name, and email.
  2. The POST APIs created in this PR take that information (as payload) from the app.
  3. The email of the user is used to find an existing user in the database.
  4. 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.
  5. 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.

Fixes #730

Type of Change:

  • Code
  • Quality Assurance
  • Outreach
  • Documentation

Code/Quality Assurance Only

  • New feature (non-breaking change which adds functionality pre-approved by mentors)

How Has This Been Tested?

Tested on local host. Google sign in done using iOS app to get id token. Then id token used in backend api and tested.

Checklist:

  • My PR follows the style guidelines of this project
  • I have performed a self-review of my own code or materials
  • I have commented my code or provided relevant documentation, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • Any dependent changes have been merged
  • Update Swagger documentation and the exported file at /docs folder
  • Update requirements.txt

Code/Quality Assurance Only

  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been published in downstream modules

@yugantarjain yugantarjain changed the title wip: Social Login wip: Social Logins Aug 10, 2020
@codecov
Copy link

codecov bot commented Aug 11, 2020

Codecov Report

Merging #719 into develop will increase coverage by 0.08%.
The diff coverage is 98.23%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #719      +/-   ##
===========================================
+ Coverage    95.84%   95.93%   +0.08%     
===========================================
  Files           95       98       +3     
  Lines         5200     5361     +161     
===========================================
+ Hits          4984     5143     +159     
- Misses         216      218       +2     
Impacted Files Coverage Δ
config.py 89.23% <ø> (ø)
tests/users/test_dao_social_sign_in.py 94.73% <94.73%> (ø)
app/api/resources/user.py 90.40% <97.91%> (+1.45%) ⬆️
tests/users/test_api_social_sign_in.py 97.91% <97.91%> (ø)
app/api/dao/user.py 86.61% <100.00%> (+1.53%) ⬆️
app/api/models/user.py 100.00% <100.00%> (ø)
app/database/models/social_sign_in.py 100.00% <100.00%> (ø)
app/database/models/user.py 98.61% <100.00%> (+0.01%) ⬆️
app/messages.py 100.00% <100.00%> (ø)
... and 3 more

Copy link
Member

@isabelcosta isabelcosta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yugantarjain good work with this PR :) This is the first time that I see code related with social login so this is a bit new to me. Here are my suggestions:

  • I suggested some improvements to the code to follow some conventions of this app.
  • Unit tests, where you may have to use mock functionalities, I am not sure
  • Can you explain in the PR description, and perhaps also in a markdown file under /docs folder, how the social login works, how to use this here (for me and others to learn)

Comment on lines 425 to 446
# 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,
)
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


token = request.json.get("id_token")
email = request.json.get("email")
client_id = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com"
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Author

@yugantarjain yugantarjain Aug 13, 2020

Choose a reason for hiding this comment

The 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?

Choose a reason for hiding this comment

The 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.
I am not sure how you can get the exact value of the config into production instance. @isabelcosta you can help clarify this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Made a constant in config.py.

Comment on lines 473 to 479
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)

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Author

@yugantarjain yugantarjain Aug 13, 2020

Choose a reason for hiding this comment

The 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.

@@ -29,6 +29,7 @@ class UserModel(db.Model):

# security
password_hash = db.Column(db.String(100))
apple_auth_id = db.Column(db.String(100))
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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?

Comment on lines +123 to +124
"username": fields.String(required=False, description="User username"),
"password": fields.String(required=False, description="User password"),
Copy link
Member

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.

Copy link
Author

@yugantarjain yugantarjain Aug 13, 2020

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.

Copy link
Member

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!

@isabelcosta isabelcosta requested a review from SanketDG August 12, 2020 23:00
@yugantarjain yugantarjain changed the title wip: Social Logins feat: Social Logins Aug 14, 2020
@vatsalkul
Copy link
Member

vatsalkul commented Aug 17, 2020

This PR is aimed to solve #730 Issue

user = DAO.create_user_using_social_login(data, provider)
# If any error occured, return error
if not isinstance(user, UserModel):
return user

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You returning a user instance here? Is this expected? in case of error, shouldn't you format it or does the user variable have the error response in which case you should also return error type (and also can you make that clear using comments?)

Copy link
Author

@yugantarjain yugantarjain Aug 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If user is not an instance of UserModel, then that means it contains the error message and http code.
Comment added to clarify the code.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we unwrap the error and http code from user variable here to make the code more readable without comment?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


token = request.json.get("id_token")
email = request.json.get("email")
client_id = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com"

Choose a reason for hiding this comment

The 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.
I am not sure how you can get the exact value of the config into production instance. @isabelcosta you can help clarify this?

@yugantarjain yugantarjain requested a review from ApheleiaS August 19, 2020 07:01
@yugantarjain
Copy link
Author

@vatsalkul just realized, the id token for apple is always the same, but google gives a new one. However, the logic remains unaffected.

config.py Outdated
@@ -1,6 +1,7 @@
import os
from datetime import timedelta

GOOGLE_AUTH_CLIENT_ID = "992237180107-lsibe891591qcubpbd8qom4fts74i5in.apps.googleusercontent.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not supposed to commit this, instead keep it configurable using an environment variable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also make sure you revoke the oAuth application associated with this, if you don't want misuse.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not supposed to commit this, instead keep it configurable using an environment variable.

I actually thought about that, but then ... not sure why I forgot to review it here. Thank you so much @SanketDG for your review!


# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not verify here? Any special usecase?

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you, I was thinking something different, not applicable here though

Copy link
Author

@yugantarjain yugantarjain Aug 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated this to - self.is_email_verified = social_login
That removes unnecessary if statements.

@yugantarjain
Copy link
Author

@isabelcosta I guess all the requested changes have been addressed.

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are using type hints in the other functions of this file, why not use here :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear about this point.

"JWT_REFRESH_TOKEN_EXPIRES"
)

# return data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# return data

Is this line for debugging purposes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is just a comment, shall I remove it?

@paritoshsinghrahar
Copy link

paritoshsinghrahar commented Aug 26, 2020

@isabelcosta @SanketDG
As the team leads for Mentorship-Backend I have a question. Since the present PR only deals with finding social login options using Google and Apple.

Should I raise new Issue under Outreach/Research for OSH:
Design a solution for integrating third-party apps authentication.

Such that someone might propose a probable uniform solution integrating more social logins like Slack, Facebook, Twitter, etc.
The proposal is only for suggestions/probable framework. Not involving coding.

If, yes. Should I break it into two or more parts? And how many hours and people should be assigned?

Copy link
Contributor

@SanketDG SanketDG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to merge!

@SanketDG
Copy link
Contributor

SanketDG commented Aug 26, 2020

Sounds good to me @paritoshsinghrahar

Such that someone might propose a probable uniform solution integrating more social logins like Slack, Facebook, Twitter, etc.
The proposal is only for suggestions/probable framework. Not involving coding.

Great!

@isabelcosta
Copy link
Member

Sounds good to me @paritoshsinghrahar

Such that someone might propose a probable uniform solution integrating more social logins like Slack, Facebook, Twitter, etc.
The proposal is only for suggestions/probable framework. Not involving coding.

Great!

@paritoshsinghrahar I +1 this :)

@techno-disaster
Copy link

@anitab-org/mentorship-backend-maintainers any update on this?

@vj-codes
Copy link
Member

@yugantarjain please resolve the merge conflicts

@isabelcosta
Copy link
Member

@vj-codes i think we can close this for now, since its inactive for a whillllle. Also the main issue should have its scope reduced, to split issues. So social login for one service (e.g.: google) and another for another service (e.g.: github). Can you help refine the issue please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Program: GSOC Related to work completed during the Google Summer of Code Program. Status: Changes Requested Changes are required to be done by the PR author.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: Implement Google social login
8 participants