diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py index d6f55e5758..195d6800be 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -80,7 +80,9 @@ class SocialLoginKey(Document): "redirect_url":"/api/method/frappe.www.login.login_via_github", "api_endpoint":"user", "api_endpoint_args":None, - "auth_url_data":None + "auth_url_data": json.dumps({ + "scope": "user:email" + }) } providers["Google"] = { diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index 880f1ee99c..73e6a072cb 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -4,6 +4,12 @@ import frappe from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError import unittest +from frappe.utils.oauth import login_via_oauth2 +from unittest.mock import patch, MagicMock +from rauth import OAuth2Service +from frappe.auth import LoginManager, CookieManager +from frappe.utils import set_request + class TestSocialLoginKey(unittest.TestCase): def test_adding_frappe_social_login_provider(self): @@ -14,6 +20,41 @@ class TestSocialLoginKey(unittest.TestCase): social_login_key.get_social_login_provider(provider_name, initialize=True) self.assertRaises(BaseUrlNotSetError, social_login_key.insert) + def test_github_login_with_private_email(self): + github_social_login_setup() + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_private_email + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token + + def test_github_login_with_public_email(self): + github_social_login_setup() + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_public_email + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token + + def test_normal_signup_and_github_login(self): + github_social_login_setup() + + if not frappe.db.exists("User", "githublogin@example.com"): + user = frappe.get_doc({ + "doctype": "User", + "email": "githublogin@example.com", + "first_name": "GitHub Login" + }) + user.save(ignore_permissions=True) + + mock_session = MagicMock() + mock_session.get.side_effect = github_response_for_login + + with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session): + login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) + def make_social_login_key(**kwargs): kwargs["doctype"] = "Social Login Key" if not "provider_name" in kwargs: @@ -34,3 +75,48 @@ def create_or_update_social_login_key(): frappe.db.commit() return social_login_key + +def create_github_social_login_key(): + if frappe.db.exists("Social Login Key", "github"): + return frappe.get_doc("Social Login Key", "github") + else: + provider_name = "GitHub" + social_login_key = make_social_login_key( + social_login_provider=provider_name + ) + social_login_key.get_social_login_provider(provider_name, initialize=True) + + # Dummy client_id and client_secret + social_login_key.client_id = "h6htd6q" + social_login_key.client_secret = "keoererk988ekkhf8w9e8ewrjhhkjer9889" + social_login_key.insert(ignore_permissions=True) + return social_login_key + +def github_response_for_private_email(url, *args, **kwargs): + if url == "user": + return_value = {"login": "dummy_username", "id": "223342", "email": None, "first_name": "Github Private"} + else: + return_value = [{"email": "github@example.com", "primary": True, "verified": True}] + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + +def github_response_for_public_email(url, *args, **kwargs): + if url == "user": + return_value = {"login": "dummy_username", "id": "223343", "email": "github_public@example.com", "first_name": "Github Public"} + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + +def github_response_for_login(url, *args, **kwargs): + if url == "user": + return_value = {"login": "dummy_username", "id": "223346", "email": None, "first_name": "Github Login"} + else: + return_value = [{"email": "githublogin@example.com", "primary": True, "verified": True}] + + return MagicMock(status_code=200, json=MagicMock(return_value=return_value)) + +def github_social_login_setup(): + set_request(path="/random") + frappe.local.cookie_manager = CookieManager() + frappe.local.login_manager = LoginManager() + + create_github_social_login_key() diff --git a/frappe/patches.txt b/frappe/patches.txt index c1b654d0e8..8309b2df57 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -184,3 +184,4 @@ frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents frappe.patches.v14_0.update_workspace2 # 20.09.2021 +frappe.patches.v14_0.update_github_endpoints #08-11-2021 diff --git a/frappe/patches/v14_0/update_github_endpoints.py b/frappe/patches/v14_0/update_github_endpoints.py new file mode 100644 index 0000000000..8f9a06a043 --- /dev/null +++ b/frappe/patches/v14_0/update_github_endpoints.py @@ -0,0 +1,10 @@ +import frappe +import json + +def execute(): + if frappe.db.exists("Social Login Key", "github"): + frappe.db.set_value("Social Login Key", "github", "auth_url_data", + json.dumps({ + "scope": "user:email" + }) + ) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index c28663d138..df2f5dca62 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -138,8 +138,14 @@ def get_info_via_oauth(provider, code, decoder=None, id_token=False): else: api_endpoint = oauth2_providers[provider].get("api_endpoint") api_endpoint_args = oauth2_providers[provider].get("api_endpoint_args") + info = session.get(api_endpoint, params=api_endpoint_args).json() + if provider == "github" and not info.get("email"): + emails = session.get("/user/emails", params=api_endpoint_args).json() + email_dict = list(filter(lambda x: x.get("primary"), emails))[0] + info["email"] = email_dict.get("email") + if not (info.get("email_verified") or info.get("email")): frappe.throw(_("Email not verified with {0}").format(provider.title()))