From 02aa7b6f412df6eca401790440f271cfffb970b7 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 3 Jan 2018 14:57:16 +0530 Subject: [PATCH] Social login refactor (#4519) * Added DocType Social Login Key WIP for https://github.com/frappe/frappe/issues/4496 added basic fields after_insert add provider_username and provider_userid fields on User dt on_trash deletes added fields on User dt * Added field to store fontawesome icon for provider * [Patch] Social Login Keys to Social Login Key * [Patch] Social Login Keys to Social Login Key * Social Login Key generates boilerplate * patch fixed for social_login_refactor * removed patch-not working * use social login keys to initiate flow * Login page shows Social Login Key * show login via if base_url present * removed boilerplate generator * Multiple Changes fix zxcvbn import in password_strength.py use of child table instead of additional fields on user dt to store username and userid * Fetched Template on Client JS * Frappe social login template working * Added Social Login Key Templates * Codacy fixes and validate social login key urls * [Patch] Social Login Keys (untested) * [Fix] Patch refactor social login keys * [Fix] Patch refactor_social_login_keys manually tested * Refactor OAuth 2.0 related changes for Social Login Key * [Fix] Patch refactor social login keys * Test - Adding Frappe Social Login Key * Social Login Key Tests check added child table entry on user for provider frappe it also checks if userid is created * [WIP] Office 365 Social Login Key Template * [Fix] Social Login - Redirect URL * [Test] Single sign-on icons for added provider * [Fix] Codacy Errors * [Fix] Social Login Key Form JS * Docs Added for Social Login Key * [Fix] Patch Refactor Social Login Keys * Handle different icon types Handle different icon types (image, icon, emoji) with just icon field * Move the login methods to a new py file frappe.integrations.oauth2_logins added copied whitelisted guest oauth2 redirect endpoints from login.py removing the functions from login.py will break backward compatibility * Social Login Key Form Changes Moved Enable field to top Fields which are not editable are collapsed * [Fix] Codacy Errors * Corrected Docs, sync.py * [Docs] Adding a social login provider * [Fix] set frappe userid from User Social Login * [Fix] frappe userid in oauth.py * removed icon_type * Use frappe.utils.is_image --- frappe/config/integrations.py | 2 +- frappe/core/doctype/user/test_user.py | 4 + frappe/core/doctype/user/user.json | 183 +---- frappe/core/doctype/user/user.py | 23 +- .../doctype/user_social_login}/__init__.py | 0 .../user_social_login/user_social_login.json | 189 +++++ .../user_social_login/user_social_login.py | 9 + frappe/docs/assets/img/social_login_key.png | Bin 0 -> 73768 bytes .../adding-social-login-provider.md | 49 ++ .../user/en/guides/app-development/index.txt | 1 + .../deployment/how-to-enable-social-logins.md | 16 + .../docs/user/en/guides/integration/index.txt | 1 + .../en/guides/integration/social_login_key.md | 26 + .../doctype/social_login_key/__init__.py | 0 .../social_login_key/social_login_key.js | 78 ++ .../social_login_key/social_login_key.json | 700 ++++++++++++++++++ .../social_login_key/social_login_key.py | 130 ++++ .../social_login_key/test_social_login_key.js | 23 + .../social_login_key/test_social_login_key.py | 24 + .../social_login_keys/social_login_keys.js | 8 - .../social_login_keys/social_login_keys.json | 414 ----------- .../social_login_keys/social_login_keys.py | 35 - frappe/integrations/oauth2.py | 9 +- frappe/integrations/oauth2_logins.py | 28 + frappe/oauth.py | 2 +- frappe/patches.txt | 3 +- .../v10_0/refactor_social_login_keys.py | 91 +++ frappe/tests/test_frappeoauth2client.py | 9 +- frappe/tests/ui/test_oauth20.py | 12 +- .../tests/ui/test_social_login_key_buttons.py | 33 + frappe/utils/html_utils.py | 22 +- frappe/utils/oauth.py | 150 ++-- frappe/utils/password_strength.py | 6 +- frappe/www/login.html | 26 +- frappe/www/login.py | 25 +- 35 files changed, 1555 insertions(+), 776 deletions(-) rename frappe/{integrations/doctype/social_login_keys => core/doctype/user_social_login}/__init__.py (100%) create mode 100644 frappe/core/doctype/user_social_login/user_social_login.json create mode 100644 frappe/core/doctype/user_social_login/user_social_login.py create mode 100644 frappe/docs/assets/img/social_login_key.png create mode 100644 frappe/docs/user/en/guides/app-development/adding-social-login-provider.md create mode 100644 frappe/docs/user/en/guides/integration/social_login_key.md create mode 100644 frappe/integrations/doctype/social_login_key/__init__.py create mode 100644 frappe/integrations/doctype/social_login_key/social_login_key.js create mode 100644 frappe/integrations/doctype/social_login_key/social_login_key.json create mode 100644 frappe/integrations/doctype/social_login_key/social_login_key.py create mode 100644 frappe/integrations/doctype/social_login_key/test_social_login_key.js create mode 100644 frappe/integrations/doctype/social_login_key/test_social_login_key.py delete mode 100644 frappe/integrations/doctype/social_login_keys/social_login_keys.js delete mode 100644 frappe/integrations/doctype/social_login_keys/social_login_keys.json delete mode 100644 frappe/integrations/doctype/social_login_keys/social_login_keys.py create mode 100644 frappe/integrations/oauth2_logins.py create mode 100644 frappe/patches/v10_0/refactor_social_login_keys.py create mode 100644 frappe/tests/ui/test_social_login_key_buttons.py diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 87f2b01614..98d17114f9 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -44,7 +44,7 @@ def get_data(): "items": [ { "type": "doctype", - "name": "Social Login Keys", + "name": "Social Login Key", "description": _("Enter keys to enable login via Facebook, Google, GitHub."), }, { diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 7d25254e4b..42f99b0bc4 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -26,6 +26,10 @@ class TestUser(unittest.TestCase): first_name='Tester')).insert() self.assertEquals(new_user.user_type, 'Website User') + # social login userid for frappe + self.assertTrue(new_user.social_logins[0].userid) + self.assertEquals(new_user.social_logins[0].provider, "frappe") + # role with desk access new_user.add_roles('_Test Role 2') new_user.save() diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 6d6eb6748d..5a2b1f5332 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1851,95 +1851,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "fb_username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Facebook Username", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "fb_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Facebook User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "google_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Google User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_49", - "fieldtype": "Column Break", + "fieldname": "social_logins", + "fieldtype": "Table", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -1947,8 +1860,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, + "label": "Social Logins", "length": 0, "no_copy": 0, + "options": "User Social Login", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1961,94 +1876,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "github_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Github User ID", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "github_username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Github Username", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frappe_userid", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frappe User ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -2124,7 +1951,7 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-11-15 13:01:00.085916", + "modified": "2017-12-29 14:37:57.759229", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5e9d33bc67..7f5375b117 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -75,8 +75,8 @@ class User(Document): if self.language == "Loading...": self.language = None - if (self.name not in ["Administrator", "Guest"]) and (not self.frappe_userid): - self.frappe_userid = frappe.generate_hash(length=39) + if (self.name not in ["Administrator", "Guest"]) and (not self.get_social_login_userid("frappe")): + self.set_social_login_userid("frappe", frappe.generate_hash(length=39)) def validate_roles(self): if self.role_profile_name: @@ -494,6 +494,25 @@ class User(Document): if len(email_accounts) != len(set(email_accounts)): frappe.throw(_("Email Account added multiple times")) + def get_social_login_userid(self, provider): + try: + for p in self.social_logins: + if p.provider == provider: + return p.userid + except: + return None + + def set_social_login_userid(self, provider, userid, username=None): + social_logins = { + "provider": provider, + "userid": userid + } + + if username: + social_logins["username"] = username + + self.append("social_logins", social_logins) + @frappe.whitelist() def get_timezones(): import pytz diff --git a/frappe/integrations/doctype/social_login_keys/__init__.py b/frappe/core/doctype/user_social_login/__init__.py similarity index 100% rename from frappe/integrations/doctype/social_login_keys/__init__.py rename to frappe/core/doctype/user_social_login/__init__.py diff --git a/frappe/core/doctype/user_social_login/user_social_login.json b/frappe/core/doctype/user_social_login/user_social_login.json new file mode 100644 index 0000000000..3cac838016 --- /dev/null +++ b/frappe/core/doctype/user_social_login/user_social_login.json @@ -0,0 +1,189 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-12-02 13:01:20.507112", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "provider", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Provider", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_0", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "userid", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "User ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-12-02 15:37:58.397062", + "modified_by": "Administrator", + "module": "Core", + "name": "User Social Login", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_social_login/user_social_login.py b/frappe/core/doctype/user_social_login/user_social_login.py new file mode 100644 index 0000000000..cc6c3d0e05 --- /dev/null +++ b/frappe/core/doctype/user_social_login/user_social_login.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class UserSocialLogin(Document): + pass diff --git a/frappe/docs/assets/img/social_login_key.png b/frappe/docs/assets/img/social_login_key.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef84703c3ecc74b3dcd5729fb000909b5ecf5c1 GIT binary patch literal 73768 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 literal 0 HcmV?d00001 diff --git a/frappe/docs/user/en/guides/app-development/adding-social-login-provider.md b/frappe/docs/user/en/guides/app-development/adding-social-login-provider.md new file mode 100644 index 0000000000..0a517af14b --- /dev/null +++ b/frappe/docs/user/en/guides/app-development/adding-social-login-provider.md @@ -0,0 +1,49 @@ +# Adding Social Login Provider + +This guide discusses how to add a social login provider to frappe via pull request. + +### Add your provider in `SocialLoginKey.get_social_login_provider` + +``` +providers["Frappe"] = { + "provider_name": "Frappe", + "enable_social_login": 1, + "custom_base_url": 1, + "icon":"/assets/frappe/images/favicon.png", + "redirect_url": "/api/method/frappe.www.login.login_via_frappe", + "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile", + "api_endpoint_args":None, + "authorize_url": "/api/method/frappe.integrations.oauth2.authorize", + "access_token_url": "/api/method/frappe.integrations.oauth2.get_token", + "auth_url_data": json.dumps({ + "response_type": "code", + "scope": "openid" + }) +} +``` + +### Add provider key in exact same type case in options of `social_login_provider` select field on `Social Login Key` DocType. e.g. `Frappe` + +Once the user adds a social login provider and enables it the `Authorization Code` is sent back by the provider api server on to the redirect_url mentioned on the same server. You will have to add a whitelisted method allowing guest access in `frappe.integrations.oauth2_logins`. e.g. `login_via_office365` + +There many implementations of OAuth 2.0 + OpenID Connect. Here we'll discuss two ways of accessing openid information. + +#### User Creation via OpenID Profile Endpoint + +example: + +``` +@frappe.whitelist(allow_guest=True) +def login_via_frappe(code, state): + login_via_oauth2("frappe", code, state, decoder=json.loads) +``` + +#### User Creation via id_token + +example: + +``` +@frappe.whitelist(allow_guest=True) +def login_via_office365(code, state): + login_via_oauth2_id_token("office_365", code, state, decoder=json.loads) +``` diff --git a/frappe/docs/user/en/guides/app-development/index.txt b/frappe/docs/user/en/guides/app-development/index.txt index 5a9b4cc4a9..6215b3d73a 100755 --- a/frappe/docs/user/en/guides/app-development/index.txt +++ b/frappe/docs/user/en/guides/app-development/index.txt @@ -13,3 +13,4 @@ single-type-doctype trigger-event-on-deletion-of-grid-row dialogs-types using-html-templates-in-javascript +adding-social-login-provider diff --git a/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md b/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md index bd16e88037..b35a5cc976 100755 --- a/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md +++ b/frappe/docs/user/en/guides/deployment/how-to-enable-social-logins.md @@ -63,4 +63,20 @@ To enable these signups, you need to have **Client ID** and **Client Secret** fr +--- + +### Office 365 + +1. Go to [https://portal.azure.com](https://portal.azure.com) +1. Create a new Azure Active Directory > App Registration. +1. Click on New Application Registration +1. Fill the form with: + - Application Name + - Application Type - Web app / API + - Single Sign-on URL as + **http://{{ yoursite }}/api/method/frappe.www.login.login\_via\_office365** +1. Enable Multi Tenent for the added App. +1. Go to the section **Application ID** and copy the Client ID and copy Client Secret by adding new password into Social Login Key + +--- diff --git a/frappe/docs/user/en/guides/integration/index.txt b/frappe/docs/user/en/guides/integration/index.txt index a2c88ed7d3..4aaf38daca 100755 --- a/frappe/docs/user/en/guides/integration/index.txt +++ b/frappe/docs/user/en/guides/integration/index.txt @@ -3,3 +3,4 @@ how_to_setup_oauth using_oauth openid_connect_and_frappe_social_login google_gsuite +social_login_key diff --git a/frappe/docs/user/en/guides/integration/social_login_key.md b/frappe/docs/user/en/guides/integration/social_login_key.md new file mode 100644 index 0000000000..8bc3d030db --- /dev/null +++ b/frappe/docs/user/en/guides/integration/social_login_key.md @@ -0,0 +1,26 @@ +# Social Login Key + +Add social login providers like Facebook, Frappe, Github, Google, Microsoft, etc and enable social login. + +#### Setup Social Logins + +To add Social Login Key go to + +> Integrations > Authentication > Social Login Key + +Social Login Key + + + +1. Select the Social Login Provider or select "Custom" +2. If required for provider enter "Base URL" +3. To enable check "Enable Social Login" to show Icon on login screen +4. Also add Client ID and Client Secret as per provider. + +e.g. Social Login Key + +- **Social Login Provider** : `Frappe` +- **Client ID** : `ABCDEFG` +- **Client Secret** : `123456` +- **Enable Social Login** : `Check` +- **Base URL** : `https://erpnext.org` (required for some providers) diff --git a/frappe/integrations/doctype/social_login_key/__init__.py b/frappe/integrations/doctype/social_login_key/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.js b/frappe/integrations/doctype/social_login_key/social_login_key.js new file mode 100644 index 0000000000..e2cbb3459f --- /dev/null +++ b/frappe/integrations/doctype/social_login_key/social_login_key.js @@ -0,0 +1,78 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt +const fields = [ + "provider_name", "base_url", "custom_base_url", + "icon", "authorize_url", "access_token_url", "redirect_url", + "api_endpoint", "api_endpoint_args", "auth_url_data" +]; + +frappe.ui.form.on('Social Login Key', { + refresh(frm) { + frm.trigger("setup_fields"); + }, + + custom_base_url(frm) { + frm.trigger("setup_fields"); + }, + + social_login_provider(frm) { + if(frm.doc.social_login_provider != "Custom") { + frappe.call({ + "doc": frm.doc, + "method": "get_social_login_provider", + "args": { + "provider": frm.doc.social_login_provider + } + }).done((r) => { + const provider = r.message; + for(var field of fields) { + frm.set_value(field, provider[field]); + frm.set_df_property(field, "read_only", 1); + if (frm.doc.custom_base_url) { + frm.toggle_enable("base_url", 1); + } + } + }); + } else { + frm.trigger("clear_fields"); + frm.trigger("setup_fields"); + } + }, + + setup_fields(frm) { + // set custom_base_url to read only for "Custom" provider + if(frm.doc.social_login_provider == "Custom") { + frm.set_value("custom_base_url", 1); + frm.set_df_property("custom_base_url", "read_only", 1); + } + + // set fields to read only for providers from template + for(var f of fields) { + if(frm.doc.social_login_provider != "Custom"){ + frm.set_df_property(f, "read_only", 1); + } + } + + // enable base_url for providers with custom_base_url + if(frm.doc.custom_base_url) { + frm.set_df_property("base_url", "read_only", 0); + frm.fields_dict["sb_identity_details"].collapse(false); + } + + // hide social_login_provider and provider_name for non local + if(!frm.doc.__islocal && + (frm.doc.social_login_provider || + frm.doc.provider_name)) { + frm.set_df_property("social_login_provider", "hidden", 1); + frm.set_df_property("provider_name", "hidden", 1); + } + }, + + clear_fields(frm) { + for(var field of fields){ + frm.set_value(field, ""); + frm.set_df_property(field, "read_only", 0); + } + } + +}); diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.json b/frappe/integrations/doctype/social_login_key/social_login_key.json new file mode 100644 index 0000000000..8475a8281b --- /dev/null +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -0,0 +1,700 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 0, + "beta": 0, + "creation": "2017-11-18 15:36:09.676722", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "enable_social_login", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Enable Social Login", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.enable_social_login", + "columns": 0, + "fieldname": "client_credentials", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client Credentials", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Custom", + "depends_on": "eval:doc.custom!=1", + "fieldname": "social_login_provider", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Social Login Provider", + "length": 0, + "no_copy": 0, + "options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 1, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "provider_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Provider Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 1, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client_secret", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client Secret", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.custom_base_url", + "columns": 0, + "depends_on": "", + "fieldname": "sb_identity_details", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Identity Details", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "icon", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Icon", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "base_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Base URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "columns": 0, + "depends_on": "", + "fieldname": "client_urls", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client URLs", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "authorize_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Authorize URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "access_token_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Access Token URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redirect_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redirect URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_endpoint", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "API Endpoint", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "custom_base_url", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Custom Base URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "columns": 0, + "depends_on": "", + "fieldname": "client_information", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Client Information", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_endpoint_args", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "API Endpoint Args", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "auth_url_data", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Auth URL Data", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-12-21 13:55:17.041059", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Social Login Key", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py new file mode 100644 index 0000000000..305da2391e --- /dev/null +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, json +from frappe import _ +from frappe.model.document import Document + +class BaseUrlNotSetError(frappe.ValidationError): pass +class AuthorizeUrlNotSetError(frappe.ValidationError): pass +class AccessTokenUrlNotSetError(frappe.ValidationError): pass +class RedirectUrlNotSetError(frappe.ValidationError): pass +class ClientIDNotSetError(frappe.ValidationError): pass +class ClientSecretNotSetError(frappe.ValidationError): pass + +class SocialLoginKey(Document): + + def autoname(self): + self.name = frappe.scrub(self.provider_name) + + def validate(self): + if self.custom_base_url and not self.base_url: + frappe.throw(_("Please enter Base URL"), exc=BaseUrlNotSetError) + if not self.authorize_url: + frappe.throw(_("Please enter Authorize URL"), exc=AuthorizeUrlNotSetError) + if not self.access_token_url: + frappe.throw(_("Please enter Access Token URL"), exc=AccessTokenUrlNotSetError) + if not self.redirect_url: + frappe.throw(_("Please enter Redirect URL"), exc=RedirectUrlNotSetError) + if self.enable_social_login and not self.client_id: + frappe.throw(_("Please enter Client ID before social login is enabled"), exc=ClientIDNotSetError) + if self.enable_social_login and not self.client_secret: + frappe.throw(_("Please enter Client Secret before social login is enabled"), exc=ClientSecretNotSetError) + + def get_social_login_provider(self, provider, initialize=False): + providers = {} + + providers["Office 365"] = { + "provider_name": "Office 365", + "enable_social_login": 1, + "base_url": "https://login.microsoftonline.com", + "custom_base_url": 0, + "icon":"fa fa-windows", + "authorize_url": "https://login.microsoftonline.com/common/oauth2/authorize", + "access_token_url": "https://login.microsoftonline.com/common/oauth2/token", + "redirect_url": "/api/method/frappe.integrations.oauth2_logins.login_via_office365", + "api_endpoint": None, + "api_endpoint_args":None, + "auth_url_data": json.dumps({ + "response_type": "code", + "scope":"openid" + }) + } + + providers["GitHub"] = { + "provider_name":"GitHub", + "enable_social_login": 1, + "base_url":"https://api.github.com/", + "custom_base_url":0, + "icon":"fa fa-github", + "authorize_url":"https://github.com/login/oauth/authorize", + "access_token_url":"https://github.com/login/oauth/access_token", + "redirect_url":"/api/method/frappe.www.login.login_via_github", + "api_endpoint":"user", + "api_endpoint_args":None, + "auth_url_data":None + } + + providers["Google"] = { + "provider_name": "Google", + "enable_social_login": 1, + "base_url": "https://www.googleapis.com", + "custom_base_url": 0, + "icon":"fa fa-google", + "authorize_url": "https://accounts.google.com/o/oauth2/auth", + "access_token_url": "https://accounts.google.com/o/oauth2/token", + "redirect_url": "/api/method/frappe.www.login.login_via_google", + "api_endpoint": "oauth2/v2/userinfo", + "api_endpoint_args":None, + "auth_url_data": json.dumps({ + "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", + "response_type": "code" + }) + } + + providers["Facebook"] = { + "provider_name": "Facebook", + "enable_social_login": 1, + "base_url": "https://graph.facebook.com", + "custom_base_url": 0, + "icon": "fa fa-facebook", + "authorize_url": "https://www.facebook.com/dialog/oauth", + "access_token_url": "https://graph.facebook.com/oauth/access_token", + "redirect_url": "/api/method/frappe.www.login.login_via_facebook", + "api_endpoint": "/v2.5/me", + "api_endpoint_args": json.dumps({ + "fields": "first_name,last_name,email,gender,location,verified,picture" + }), + "auth_url_data": json.dumps({ + "display": "page", + "response_type": "code", + "scope": "email,public_profile" + }) + } + + providers["Frappe"] = { + "provider_name": "Frappe", + "enable_social_login": 1, + "custom_base_url": 1, + "icon":"/assets/frappe/images/favicon.png", + "redirect_url": "/api/method/frappe.www.login.login_via_frappe", + "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile", + "api_endpoint_args":None, + "authorize_url": "/api/method/frappe.integrations.oauth2.authorize", + "access_token_url": "/api/method/frappe.integrations.oauth2.get_token", + "auth_url_data": json.dumps({ + "response_type": "code", + "scope": "openid" + }) + } + + # Initialize the doc and return, used in patch + # Or can be used for creating key from controller + if initialize and provider: + for k, v in providers[provider].items(): + setattr(self,k,v) + return + + return providers.get(provider) if provider else providers diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.js b/frappe/integrations/doctype/social_login_key/test_social_login_key.js new file mode 100644 index 0000000000..86aad7ab64 --- /dev/null +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Social Login Key", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Social Login Key + () => frappe.tests.make('Social Login Key', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); 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 new file mode 100644 index 0000000000..58bd48d64a --- /dev/null +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError +import unittest + +class TestSocialLoginKey(unittest.TestCase): + def test_adding_frappe_social_login_provider(self): + provider_name = "Frappe" + social_login_key = make_social_login_key( + social_login_provider=provider_name + ) + social_login_key.get_social_login_provider(provider_name, initialize=True) + self.assertRaises(BaseUrlNotSetError, social_login_key.insert) + +def make_social_login_key(**kwargs): + kwargs["doctype"] = "Social Login Key" + if not "provider_name" in kwargs: + kwargs["provider_name"] = "Test OAuth2 Provider" + doc = frappe.get_doc(kwargs) + return doc diff --git a/frappe/integrations/doctype/social_login_keys/social_login_keys.js b/frappe/integrations/doctype/social_login_keys/social_login_keys.js deleted file mode 100644 index 52ced2447a..0000000000 --- a/frappe/integrations/doctype/social_login_keys/social_login_keys.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Social Login Keys', { - refresh: function(frm) { - - } -}); diff --git a/frappe/integrations/doctype/social_login_keys/social_login_keys.json b/frappe/integrations/doctype/social_login_keys/social_login_keys.json deleted file mode 100644 index 9ce8c3ad79..0000000000 --- a/frappe/integrations/doctype/social_login_keys/social_login_keys.json +++ /dev/null @@ -1,414 +0,0 @@ -{ - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-03-04 08:29:52", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "facebook", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Facebook", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "facebook_client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Facebook Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "facebook_client_secret", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Facebook Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "google", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Google", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "google_client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Google Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "google_client_secret", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Google Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "github", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GitHub", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "github_client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GitHub Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "github_client_secret", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GitHub Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frappe", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frappe", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frappe_client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frappe Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frappe_client_secret", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frappe Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frappe_server_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Frappe Server URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-signin", - "idx": 1, - "image_view": 0, - "in_create": 0, - "in_dialog": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:30.397643", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Social Login Keys", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/social_login_keys/social_login_keys.py b/frappe/integrations/doctype/social_login_keys/social_login_keys.py deleted file mode 100644 index 33c8ab2560..0000000000 --- a/frappe/integrations/doctype/social_login_keys/social_login_keys.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import requests -import socket - -from frappe.model.document import Document -from frappe import _ -from six.moves.urllib.parse import urlparse - -class SocialLoginKeys(Document): - def validate(self): - self.validate_frappe_server_url() - - def validate_frappe_server_url(self): - if self.frappe_server_url: - if self.frappe_server_url.endswith('/'): - self.frappe_server_url = self.frappe_server_url[:-1] - - try: - frappe_server_hostname = urlparse(self.frappe_server_url).netloc - except: - frappe.throw(_("Check Frappe Server URL")) - - if socket.gethostname() != frappe_server_hostname or \ - (frappe.local.conf.domains is not None) and \ - (frappe_server_hostname not in frappe.local.conf.domains): - try: - requests.get(self.frappe_server_url + "/api/method/frappe.handler.version", timeout=5) - except: - frappe.throw(_("Unable to make request to the Frappe Server URL")) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 7a5b616395..5bc7f33094 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -103,9 +103,9 @@ def get_token(*args, **kwargs): headers = r.headers #Check whether frappe server URL is set - frappe_server_url = frappe.db.get_value("Social Login Keys", None, "frappe_server_url") or None + frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None if not frappe_server_url: - frappe.throw(_("Define Frappe Server URL in Social Login Keys")) + frappe.throw(_("Please set Base URL in Social Login Key for Frappe")) try: headers, body, status = get_oauth_server().create_token_response(uri, http_method, body, headers, frappe.flags.oauth_credentials) @@ -124,7 +124,7 @@ def get_token(*args, **kwargs): id_token = { "aud": token_client, "exp": int((frappe.db.get_value("OAuth Bearer Token", out.access_token, "expiration_time") - frappe.utils.datetime.datetime(1970, 1, 1)).total_seconds()), - "sub": frappe.db.get_value("User", token_user, "frappe_userid"), + "sub": frappe.db.get_value("User Social Login", {"parent":token_user, "provider": "frappe"}, "userid"), "iss": frappe_server_url, "at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256) } @@ -156,7 +156,8 @@ def revoke_token(*args, **kwargs): @frappe.whitelist() def openid_profile(*args, **kwargs): picture = None - first_name, last_name, avatar, name, frappe_userid = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name", "frappe_userid"]) + first_name, last_name, avatar, name = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name"]) + frappe_userid = frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid") request_url = urlparse(frappe.request.url) if avatar: diff --git a/frappe/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py new file mode 100644 index 0000000000..666a0030ef --- /dev/null +++ b/frappe/integrations/oauth2_logins.py @@ -0,0 +1,28 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +import frappe.utils +from frappe.utils.oauth import login_via_oauth2, login_via_oauth2_id_token +import json + +@frappe.whitelist(allow_guest=True) +def login_via_google(code, state): + login_via_oauth2("google", code, state, decoder=json.loads) + +@frappe.whitelist(allow_guest=True) +def login_via_github(code, state): + login_via_oauth2("github", code, state) + +@frappe.whitelist(allow_guest=True) +def login_via_facebook(code, state): + login_via_oauth2("facebook", code, state, decoder=json.loads) + +@frappe.whitelist(allow_guest=True) +def login_via_frappe(code, state): + login_via_oauth2("frappe", code, state, decoder=json.loads) + +@frappe.whitelist(allow_guest=True) +def login_via_office365(code, state): + login_via_oauth2_id_token("office_365", code, state, decoder=json.loads) diff --git a/frappe/oauth.py b/frappe/oauth.py index 61b4db5034..e7944da06b 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -387,7 +387,7 @@ class OAuthWebRequestValidator(RequestValidator): - OpenIDConnectImplicit - OpenIDConnectHybrid """ - if id_token_hint and id_token_hint == frappe.get_value("User", frappe.session.user, "frappe_userid"): + if id_token_hint and id_token_hint == frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid"): return True else: return False diff --git a/frappe/patches.txt b/frappe/patches.txt index 49bc7bb978..a1e79e62c8 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -200,4 +200,5 @@ frappe.patches.v9_1.resave_domain_settings frappe.patches.v9_1.revert_domain_settings frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) -frappe.patches.v10_0.reload_countries_and_currencies \ No newline at end of file +frappe.patches.v10_0.reload_countries_and_currencies +frappe.patches.v10_0.refactor_social_login_keys diff --git a/frappe/patches/v10_0/refactor_social_login_keys.py b/frappe/patches/v10_0/refactor_social_login_keys.py new file mode 100644 index 0000000000..9f1f0b4afc --- /dev/null +++ b/frappe/patches/v10_0/refactor_social_login_keys.py @@ -0,0 +1,91 @@ +# see license +import frappe + +def execute(): + # Move User Data into DocType + frappe.reload_doc("core", "doctype", "user", force=True) + frappe.reload_doc("core", "doctype", "user_social_login", force=True) + users = frappe.get_all("User", filters=[["username", "not in", ["Guest","Administrator"]]]) + for u in users: + user = frappe.get_doc("User", u.get("name")) + save = False + + if user.fb_userid and user.fb_username: + user.append("social_logins", { + "provider": "facebook", + "userid": user.fb_userid, + "username": user.fb_username + }) + save = True + + if user.frappe_userid: + user.append("social_logins", { + "provider": "frappe", + "userid": user.frappe_userid + }) + save = True + + if user.github_userid and user.github_username: + user.append("social_logins", { + "provider": "github", + "userid": user.github_userid, + "username": user.github_username, + }) + save = True + + if user.google_userid: + user.append("social_logins", { + "provider": "google", + "userid": user.google_userid, + }) + save = True + + if save: + user.save() + + # Create Social Login Key(s) from Social Login Keys + frappe.reload_doc("integrations", "doctype", "social_login_key", force=True) + + social_login_keys = frappe.get_doc("Social Login Keys", "Social Login Keys") + if social_login_keys.facebook_client_id or social_login_keys.facebook_client_secret: + facebook_login_key = frappe.new_doc("Social Login Key") + facebook_login_key.get_social_login_provider("Facebook", initialize=True) + facebook_login_key.social_login_provider = "Facebook" + facebook_login_key.client_id = social_login_keys.facebook_client_id + facebook_login_key.client_secret = social_login_keys.facebook_client_secret + if not (facebook_login_key.client_secret and facebook_login_key.client_id): + facebook_login_key.enable_social_login = 0 + facebook_login_key.save() + + if social_login_keys.frappe_server_url: + frappe_login_key = frappe.new_doc("Social Login Key") + frappe_login_key.get_social_login_provider("Frappe", initialize=True) + frappe_login_key.social_login_provider = "Frappe" + frappe_login_key.base_url = social_login_keys.frappe_server_url + frappe_login_key.client_id = social_login_keys.frappe_client_id + frappe_login_key.client_secret = social_login_keys.frappe_client_secret + if not (frappe_login_key.client_secret and frappe_login_key.client_id and frappe_login_key.base_url): + frappe_login_key.enable_social_login = 0 + frappe_login_key.save() + + if social_login_keys.github_client_id or social_login_keys.github_client_secret: + github_login_key = frappe.new_doc("Social Login Key") + github_login_key.get_social_login_provider("GitHub", initialize=True) + github_login_key.social_login_provider = "GitHub" + github_login_key.client_id = social_login_keys.github_client_id + github_login_key.client_secret = social_login_keys.github_client_secret + if not (github_login_key.client_secret and github_login_key.client_id): + github_login_key.enable_social_login = 0 + github_login_key.save() + + if social_login_keys.google_client_id or social_login_keys.google_client_secret: + google_login_key = frappe.new_doc("Social Login Key") + google_login_key.get_social_login_provider("Google", initialize=True) + google_login_key.social_login_provider = "Google" + google_login_key.client_id = social_login_keys.google_client_id + google_login_key.client_secret = social_login_keys.google_client_secret + if not (google_login_key.client_secret and google_login_key.client_id): + google_login_key.enable_social_login = 0 + google_login_key.save() + + frappe.delete_doc("DocType", "Social Login Keys") diff --git a/frappe/tests/test_frappeoauth2client.py b/frappe/tests/test_frappeoauth2client.py index df85c33ad0..ebf09adf6d 100644 --- a/frappe/tests/test_frappeoauth2client.py +++ b/frappe/tests/test_frappeoauth2client.py @@ -16,8 +16,13 @@ class TestFrappeOAuth2Client(unittest.TestCase): self.client_id = frappe.get_all("OAuth Client", fields=["*"])[0].get("client_id") # Set Frappe server URL reqired for id_token generation - frappe.db.set_value("Social Login Keys", None, "frappe_server_url", "http://localhost:8000") - frappe.db.commit() + try: + frappe_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + frappe_login_key = frappe.new_doc("Social Login Key") + frappe_login_key.get_social_login_provider("Frappe", initialize=True) + frappe_login_key.base_url = "http://localhost:8000" + frappe_login_key.save() def test_insert_note(self): diff --git a/frappe/tests/ui/test_oauth20.py b/frappe/tests/ui/test_oauth20.py index c6361fc39d..1cc864416e 100644 --- a/frappe/tests/ui/test_oauth20.py +++ b/frappe/tests/ui/test_oauth20.py @@ -15,8 +15,13 @@ class TestOAuth20(unittest.TestCase): self.client_id = frappe.get_all("OAuth Client", fields=["*"])[0].get("client_id") # Set Frappe server URL reqired for id_token generation - frappe.db.set_value("Social Login Keys", None, "frappe_server_url", "http://localhost:8000") - frappe.db.commit() + try: + frappe_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + frappe_login_key = frappe.new_doc("Social Login Key") + frappe_login_key.get_social_login_provider("Frappe", initialize=True) + frappe_login_key.base_url = "http://localhost:8000" + frappe_login_key.save() def test_login_using_authorization_code(self): @@ -113,3 +118,6 @@ class TestOAuth20(unittest.TestCase): self.assertTrue(response_url.get("expires_in")) self.assertTrue(response_url.get("scope")) self.assertTrue(response_url.get("token_type")) + + def tearDown(self): + self.driver.close() diff --git a/frappe/tests/ui/test_social_login_key_buttons.py b/frappe/tests/ui/test_social_login_key_buttons.py new file mode 100644 index 0000000000..376d4139e4 --- /dev/null +++ b/frappe/tests/ui/test_social_login_key_buttons.py @@ -0,0 +1,33 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import unittest, frappe, time +from frappe.utils.selenium_testdriver import TestDriver + +class TestSocialLoginKeyButtons(unittest.TestCase): + def setUp(self): + try: + frappe_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + frappe_login_key = frappe.new_doc("Social Login Key") + frappe_login_key.get_social_login_provider("Frappe", initialize=True) + frappe_login_key.base_url = "http://localhost:8000" + frappe_login_key.enable_social_login = 1 + frappe_login_key.client_id = "test_client_id" + frappe_login_key.client_secret = "test_client_secret" + frappe_login_key.save() + + self.driver = TestDriver() + + def test_login_buttons(self): + + # Go to Login Page + self.driver.get("login") + + time.sleep(2) + frappe_social_login = self.driver.find(".btn-frappe") + self.assertTrue(len(frappe_social_login) > 0) + + def tearDown(self): + self.driver.close() diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 3daa87d360..a45c85780b 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -1,4 +1,4 @@ -import json +import json, re import bleach, bleach_whitelist.bleach_whitelist as bleach_whitelist from six import string_types @@ -48,6 +48,24 @@ def is_json(text): else: return True +def get_icon_html(icon, small=False): + emoji_pattern = re.compile(u'[' + u'\U0001F300-\U0001F64F' + u'\U0001F680-\U0001F6FF' + u'\u2600-\u26FF\u2700-\u27BF]+', + re.UNICODE) + + if icon and emoji_pattern.match(icon): + return '' + icon + '' + + if frappe.utils.is_image(icon): + return \ + ''.format(icon=icon) \ + if small else \ + ''.format(icon=icon) + else: + return "".format(icon=icon) + # adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py acceptable_elements = [ 'a', 'abbr', 'acronym', 'address', 'area', @@ -149,4 +167,4 @@ svg_attributes = [ 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', 'y1', 'y2', 'zoomAndPan' -] \ No newline at end of file +] diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index 61498a9722..803aad2ac3 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -4,91 +4,36 @@ from __future__ import unicode_literals import frappe import frappe.utils -import json +import json, jwt from frappe import _ +from frappe.utils.password import get_decrypted_password from six import string_types class SignupDisabledError(frappe.PermissionError): pass def get_oauth2_providers(): - out = { - "google": { + out = {} + providers = frappe.get_all("Social Login Key", fields=["*"]) + for provider in providers: + authorize_url, access_token_url = provider.authorize_url, provider.access_token_url + if provider.custom_base_url: + authorize_url = provider.base_url + provider.authorize_url + access_token_url = provider.base_url + provider.access_token_url + out[provider.name] = { "flow_params": { - "name": "google", - "authorize_url": "https://accounts.google.com/o/oauth2/auth", - "access_token_url": "https://accounts.google.com/o/oauth2/token", - "base_url": "https://www.googleapis.com", - }, - - "redirect_uri": "/api/method/frappe.www.login.login_via_google", - - "auth_url_data": { - "scope": "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", - "response_type": "code" - }, - - # relative to base_url - "api_endpoint": "oauth2/v2/userinfo" - }, - - "github": { - "flow_params": { - "name": "github", - "authorize_url": "https://github.com/login/oauth/authorize", - "access_token_url": "https://github.com/login/oauth/access_token", - "base_url": "https://api.github.com/" - }, - - "redirect_uri": "/api/method/frappe.www.login.login_via_github", - - # relative to base_url - "api_endpoint": "user" - }, - - "facebook": { - "flow_params": { - "name": "facebook", - "authorize_url": "https://www.facebook.com/dialog/oauth", - "access_token_url": "https://graph.facebook.com/oauth/access_token", - "base_url": "https://graph.facebook.com" - }, - - "redirect_uri": "/api/method/frappe.www.login.login_via_facebook", - - "auth_url_data": { - "display": "page", - "response_type": "code", - "scope": "email,public_profile" - }, - - # relative to base_url - "api_endpoint": "/v2.5/me", - "api_endpoint_args": { - "fields": "first_name,last_name,email,gender,location,verified,picture" + "name": provider.name, + "authorize_url": authorize_url, + "access_token_url": access_token_url, + "base_url": provider.base_url }, + "redirect_uri": provider.redirect_url, + "api_endpoint": provider.api_endpoint, } - } + if provider.auth_url_data: + out[provider.name]["auth_url_data"] = json.loads(provider.auth_url_data) - frappe_server_url = frappe.db.get_value("Social Login Keys", None, "frappe_server_url") - if frappe_server_url: - out['frappe'] = { - "flow_params": { - "name": "frappe", - "authorize_url": frappe_server_url + "/api/method/frappe.integrations.oauth2.authorize", - "access_token_url": frappe_server_url + "/api/method/frappe.integrations.oauth2.get_token", - "base_url": frappe_server_url - }, - - "redirect_uri": "/api/method/frappe.www.login.login_via_frappe", - - "auth_url_data": { - "response_type": "code", - "scope": "openid" - }, - - # relative to base_url - "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile" - } + if provider.api_endpoint_args: + out[provider.name]["api_endpoint_args"] = json.loads(provider.api_endpoint_args) return out @@ -100,17 +45,13 @@ def get_oauth_keys(provider): if not keys: # try database - social = frappe.get_doc("Social Login Keys", "Social Login Keys") - keys = {} - for fieldname in ("client_id", "client_secret"): - value = social.get("{provider}_{fieldname}".format(provider=provider, fieldname=fieldname)) - if not value: - keys = {} - break - keys[fieldname] = value - + client_id, client_secret = frappe.get_value("Social Login Key", provider, ["client_id", "client_secret"]) + client_secret = get_decrypted_password("Social Login Key", provider, "client_secret") + keys = { + "client_id": client_id, + "client_secret": client_secret + } return keys - else: return { "client_id": keys["client_id"], @@ -169,7 +110,11 @@ def login_via_oauth2(provider, code, state, decoder=None): info = get_info_via_oauth(provider, code, decoder) login_oauth_user(info, provider=provider, state=state) -def get_info_via_oauth(provider, code, decoder=None): +def login_via_oauth2_id_token(provider, code, state, decoder=None): + info = get_info_via_oauth(provider, code, decoder, id_token=True) + login_oauth_user(info, provider=provider, state=state) + +def get_info_via_oauth(provider, code, decoder=None, id_token=False): flow = get_oauth2_flow(provider) oauth2_providers = get_oauth2_providers() @@ -186,9 +131,14 @@ def get_info_via_oauth(provider, code, decoder=None): session = flow.get_auth_session(**args) - 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 id_token: + parsed_access = json.loads(session.access_token_response.text) + token = parsed_access['id_token'] + info = jwt.decode(token, flow.client_secret, verify=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 (("verified_email" in info and not info.get("verified_email")) or ("verified" in info and not info.get("verified"))): @@ -289,26 +239,28 @@ def update_oauth_user(user, data, provider): frappe.respond_as_web_page(_('Not Allowed'), _('User {0} is disabled').format(user.email)) return False - if provider=="facebook" and not user.get("fb_userid"): + if provider=="facebook" and not user.get_social_login_userid(provider): save = True + user.set_social_login_userid(provider, userid=data["id"], username=data.get("username")) user.update({ - "fb_username": data.get("username"), - "fb_userid": data["id"], "user_image": "https://graph.facebook.com/{id}/picture".format(id=data["id"]) }) - elif provider=="google" and not user.get("google_userid"): + elif provider=="google" and not user.get_social_login_userid(provider): save = True - user.google_userid = data["id"] + user.set_social_login_userid(provider, userid=data["id"]) - elif provider=="github" and not user.get("github_userid"): + elif provider=="github" and not user.get_social_login_userid(provider): save = True - user.github_userid = data["id"] - user.github_username = data["login"] + user.set_social_login_userid(provider, userid=data["id"], username=data.get("login")) - elif provider=="frappe" and not user.get("frappe_userid"): + elif provider=="frappe" and not user.get_social_login_userid(provider): save = True - user.frappe_userid = data["sub"] + user.set_social_login_userid(provider, userid=data["sub"]) + + elif provider=="office_365" and not user.get_social_login_userid(provider): + save = True + user.set_social_login_userid(provider, userid=data["sub"]) if save: user.flags.ignore_permissions = True diff --git a/frappe/utils/password_strength.py b/frappe/utils/password_strength.py index a6b8f3f2a9..2e781897ad 100644 --- a/frappe/utils/password_strength.py +++ b/frappe/utils/password_strength.py @@ -3,7 +3,11 @@ from __future__ import unicode_literals -from zxcvbn import zxcvbn +try: + from zxcvbn import zxcvbn +except Exception as e: + import zxcvbn + import frappe from frappe import _ diff --git a/frappe/www/login.html b/frappe/www/login.html index 8cfbfea25b..cd85d3086f 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -39,27 +39,11 @@
{{ _("Or login with") }}

- {%- if facebook_login is defined %} - - {{ _("Facebook") }} - {%- endif -%} - - {%- if google_login is defined %} - - {{ _("Google") }} - {%- endif -%} - - {%- if github_login is defined %} - - {{ _("GitHub") }} - {%- endif -%} - - {%- if frappe_login is defined %} - - {{ _("Frappe") }} - {%- endif -%} + {% for provider in provider_logins %} + + {{ provider.icon }} {{ provider.provider_name }} + {% endfor %}

{%- endif -%} diff --git a/frappe/www/login.py b/frappe/www/login.py index 8c83ec783b..e14dd85cf0 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe import frappe.utils -from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_via_oauth2, login_oauth_user as _login_oauth_user, redirect_post_login +from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_via_oauth2, login_via_oauth2_id_token, login_oauth_user as _login_oauth_user, redirect_post_login import json from frappe import _ from frappe.auth import LoginManager from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_settings +from frappe.utils.password import get_decrypted_password +from frappe.utils.html_utils import get_icon_html no_cache = True @@ -21,11 +23,20 @@ def get_context(context): context.no_header = True context.for_test = 'login.html' context["title"] = "Login" + context["provider_logins"] = [] context["disable_signup"] = frappe.utils.cint(frappe.db.get_value("Website Settings", "Website Settings", "disable_signup")) - - for provider in ("google", "github", "facebook", "frappe"): - if get_oauth_keys(provider): - context["{provider}_login".format(provider=provider)] = get_oauth2_authorize_url(provider) + providers = [i.name for i in frappe.get_all("Social Login Key", filters={"enable_social_login":1})] + for provider in providers: + client_id, base_url = frappe.get_value("Social Login Key", provider, ["client_id", "base_url"]) + client_secret = get_decrypted_password("Social Login Key", provider, "client_secret") + icon = get_icon_html(frappe.get_value("Social Login Key", provider, "icon"), small=True) + if (get_oauth_keys(provider) and client_secret and client_id and base_url): + context.provider_logins.append({ + "name": provider, + "provider_name": frappe.get_value("Social Login Key", provider, "provider_name"), + "auth_url": get_oauth2_authorize_url(provider), + "icon": icon + }) context["social_login"] = True ldap_settings = get_ldap_settings() @@ -59,6 +70,10 @@ def login_via_facebook(code, state): def login_via_frappe(code, state): login_via_oauth2("frappe", code, state, decoder=json.loads) +@frappe.whitelist(allow_guest=True) +def login_via_office365(code, state): + login_via_oauth2_id_token("office_365", code, state, decoder=json.loads) + @frappe.whitelist(allow_guest=True) def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False): if not ((data and provider and state) or (email_id and key)):