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 0000000000..4ef84703c3 Binary files /dev/null and b/frappe/docs/assets/img/social_login_key.png differ 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)):