From 9f61571c0c35b5b442b5060b8a9683d8dec7b885 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Thu, 27 Feb 2014 20:38:09 +0530 Subject: [PATCH] Commonified OAuth v2 based login using Facebook, Google and GitHub --- frappe/core/doctype/profile/profile.py | 7 +- frappe/core/doctype/profile/profile.txt | 18 +- frappe/data/sample_site_config.json | 30 +++ frappe/templates/includes/login.js | 62 ------ frappe/templates/pages/login.html | 30 +-- frappe/templates/pages/login.py | 201 ++++++++++++------ .../doctype/blog_category/blog_category.py | 2 +- frappe/website/render.py | 3 +- frappe/website/sitemap.py | 2 - requirements.txt | 2 +- 10 files changed, 212 insertions(+), 145 deletions(-) create mode 100644 frappe/data/sample_site_config.json diff --git a/frappe/core/doctype/profile/profile.py b/frappe/core/doctype/profile/profile.py index 0d281b9a0e..71cbf5aedc 100644 --- a/frappe/core/doctype/profile/profile.py +++ b/frappe/core/doctype/profile/profile.py @@ -110,11 +110,8 @@ class DocType: def update_gravatar(self): import md5 if not self.doc.user_image: - if self.doc.fb_username: - self.doc.user_image = "https://graph.facebook.com/" + self.doc.fb_username + "/picture" - else: - self.doc.user_image = "https://secure.gravatar.com/avatar/" + md5.md5(self.doc.name).hexdigest() \ - + "?d=retro" + self.doc.user_image = "https://secure.gravatar.com/avatar/" + md5.md5(self.doc.name).hexdigest() \ + + "?d=retro" def reset_password(self): from frappe.utils import random_string, get_url diff --git a/frappe/core/doctype/profile/profile.txt b/frappe/core/doctype/profile/profile.txt index 63cd6407b3..9fdd7675c1 100644 --- a/frappe/core/doctype/profile/profile.txt +++ b/frappe/core/doctype/profile/profile.txt @@ -2,7 +2,7 @@ { "creation": "2013-03-07 11:54:44", "docstatus": 0, - "modified": "2014-02-26 17:40:31", + "modified": "2014-02-27 18:01:32", "modified_by": "Administrator", "owner": "Administrator" }, @@ -484,6 +484,22 @@ "permlevel": 0, "read_only": 1 }, + { + "doctype": "DocField", + "fieldname": "github_userid", + "fieldtype": "Data", + "label": "Github User ID", + "permlevel": 0, + "read_only": 1 + }, + { + "doctype": "DocField", + "fieldname": "github_username", + "fieldtype": "Data", + "label": "Github Username", + "permlevel": 0, + "read_only": 1 + }, { "create": 1, "doctype": "DocPerm", diff --git a/frappe/data/sample_site_config.json b/frappe/data/sample_site_config.json new file mode 100644 index 0000000000..93f4d07147 --- /dev/null +++ b/frappe/data/sample_site_config.json @@ -0,0 +1,30 @@ +{ + "db_name": "testdb", + "db_password": "password", + "mute_emails": true, + + "developer_mode": 1, + "auto_cache_clear": true, + "disable_website_cache": true, + "max_file_size": 1000000, + + "mail_server": "localhost", + "mail_login": null, + "mail_password": null, + "mail_port": 25, + "use_ssl": 0, + "auto_email_id": "hello@example.com", + + "google_sign_in": { + "client_id": "google_client_id", + "client_secret": "google_client_secret" + }, + "github_sign_in": { + "client_id": "github_client_id", + "client_secret": "github_client_secret" + }, + "facebook_sign_in": { + "client_id": "facebook_client_id", + "client_secret": "facebook_client_secret" + } +} \ No newline at end of file diff --git a/frappe/templates/includes/login.js b/frappe/templates/includes/login.js index d6b1275fcb..2d6d5b0246 100644 --- a/frappe/templates/includes/login.js +++ b/frappe/templates/includes/login.js @@ -112,65 +112,3 @@ frappe.ready(function() { login.login(); $(document).trigger('login_rendered'); }); - -{% if fb_app_id is defined -%} - // facebook login - -$(function() { - $login = $(".btn-facebook").prop("disabled", true); - $.getScript('//connect.facebook.net/en_UK/all.js', function() { - $login.prop("disabled", false); - FB.init({ - appId: '{{ fb_app_id }}', - }); - $login.click(function() { - $login.prop("disabled", true).html("Logging In..."); - login.via_facebook(); - }); - }); -}); - -login.via_facebook = function() { - // not logged in to facebook either - FB.login(function(response) { - if (response.authResponse) { - // yes logged in via facebook - console.log('Welcome! Fetching your information.... '); - var fb_access_token = response.authResponse.accessToken; - - // get user graph - FB.api('/me', function(response) { - response.fb_access_token = fb_access_token || "[none]"; - $.ajax({ - url:"/", - type: "POST", - data: { - cmd:"frappe.templates.pages.login.login_via_facebook", - data: JSON.stringify(response) - }, - statusCode: login.login_handlers - }) - }); - } else { - frappe.msgprint("You have denied access to this application via Facebook. \ - Please change your privacy settings in Facebook and try again. \ - If you do not want to use Facebook login, sign-up here"); - } - },{scope:"email"}); -} - -frappe.ready(function() { - var user_id = frappe.get_cookie("user_id"); - var sid = frappe.get_cookie("sid"); - - // logged in? - if(!sid || sid==="Guest") { - // fallback on facebook login -- no login again - $(".btn-facebook").removeAttr("disabled"); - } else { - // get private stuff (if access) - // app.setup_user({"user": user_id}); - } -}); - -{%- endif %} diff --git a/frappe/templates/pages/login.html b/frappe/templates/pages/login.html index 8b3616a560..4325a195f0 100644 --- a/frappe/templates/pages/login.html +++ b/frappe/templates/pages/login.html @@ -13,19 +13,25 @@ -->
- {%- if fb_app_id is defined -%} -
-

-

or

-

+ {%- if third_party_sign_in -%} +

or login via

+

+ {%- if facebook_sign_in is defined %} + + {{ _("Facebook") }} + {%- endif -%} + + {%- if google_sign_in is defined %} + + {{ _("Google") }} + {%- endif -%} + + {%- if github_sign_in is defined %} + + {{ _("GitHub") }} + {%- endif -%} +

{%- endif -%} - {%- if google_sign_in is defined %} -

-

or

- - {{ _("Login via Google") }}

- {%- endif -%}


{{ _("Not a user yet? Sign up") }}

diff --git a/frappe/templates/pages/login.py b/frappe/templates/pages/login.py index 7edd544e8e..b57089f3f2 100644 --- a/frappe/templates/pages/login.py +++ b/frappe/templates/pages/login.py @@ -6,114 +6,195 @@ import frappe, os import httplib2 import json from werkzeug.utils import redirect +import frappe.utils no_cache = True def get_context(context): # get settings from site config context["title"] = "Login" - if frappe.conf.get("fb_app_id"): - context.update({ "fb_app_id": frappe.conf.fb_app_id }) + + for provider in ("google", "github", "facebook"): + if get_oauth_keys(provider): + context["{provider}_sign_in".format(provider=provider)] = get_oauth2_authorize_url(provider) + context["third_party_sign_in"] = True + + return context + +oauth2_providers = { + "google": { + "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", + }, - if os.path.exists(frappe.get_site_path("google_config.json")): - context.update({ "google_sign_in": get_google_auth_url() }) + "redirect_uri": "/api/method/frappe.templates.pages.login.login_via_google", - return context + "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.templates.pages.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.templates.pages.login.login_via_facebook", + + "auth_url_data": { + "display": "page", + "response_type": "code", + "scope": "email,user_birthday" + }, + + # relative to base_url + "api_endpoint": "me" + } +} -def get_google_auth_url(): - flow = get_google_auth_flow() - return flow.step1_get_authorize_url() +def get_oauth_keys(provider): + # get client_id and client_secret from conf + return frappe.conf.get("{provider}_sign_in".format(provider=provider)) -def get_google_auth_flow(): - from oauth2client.client import flow_from_clientsecrets - google_config_path = frappe.get_site_path("google_config.json") - google_config = frappe.get_file_json(google_config_path) - - flow = flow_from_clientsecrets(google_config_path, - scope=['https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email'], - redirect_uri=google_config.get("web").get("redirect_uris")[0]) +def get_oauth2_authorize_url(provider): + flow = get_oauth2_flow(provider) + + # relative to absolute url + data = { "redirect_uri": get_redirect_uri(provider) } - return flow + # additional data if any + data.update(oauth2_providers[provider].get("auth_url_data", {})) + + return flow.get_authorize_url(**data) + +def get_oauth2_flow(provider): + from rauth import OAuth2Service + + # get client_id and client_secret + params = get_oauth_keys(provider) + + # additional params for getting the flow + params.update(oauth2_providers[provider]["flow_params"]) + + # and we have setup the communication lines + return OAuth2Service(**params) + +def get_redirect_uri(provider): + redirect_uri = oauth2_providers[provider]["redirect_uri"] + return frappe.utils.get_url(redirect_uri) @frappe.whitelist(allow_guest=True) def login_via_google(code): - flow = get_google_auth_flow() - credentials = flow.step2_exchange(code) + login_via_oauth2("google", code, decoder=json.loads) + +@frappe.whitelist(allow_guest=True) +def login_via_github(code): + login_via_oauth2("github", code) - http = httplib2.Http() - http = credentials.authorize(http) +@frappe.whitelist(allow_guest=True) +def login_via_facebook(code): + login_via_oauth2("facebook", code) - resp, content = http.request('https://www.googleapis.com/oauth2/v2/userinfo', 'GET') - info = json.loads(content) +def login_via_oauth2(provider, code, decoder=None): + flow = get_oauth2_flow(provider) - if not info.get("verified_email"): - frappe.throw("You need to verify your email with Google before you can proceed.") + args = { + "data": { + "code": code, + "redirect_uri": get_redirect_uri(provider), + "grant_type": "authorization_code" + } + } + if decoder: + args["decoder"] = decoder - frappe.local._response = redirect("/") + session = flow.get_auth_session(**args) - login_oauth_user(info, oauth_provider="google") + api_endpoint = oauth2_providers[provider].get("api_endpoint") + info = session.get(api_endpoint).json() - # because of a GET request! - frappe.db.commit() + print info -@frappe.whitelist(allow_guest=True) -def login_via_facebook(data): - data = json.loads(data) + if "verified_email" in info and not info.get("verified_email"): + frappe.throw("{verify}: {provider}".format( + verify=_("Error. Please verify your email with"), + provider=provider.title())) - if not (data.get("id") and data.get("fb_access_token")): - raise frappe.ValidationError - - if not get_fb_userid(data.get("fb_access_token")): - # garbage - raise frappe.ValidationError - - login_oauth_user(data, oauth_provider="facebook") + login_oauth_user(info, provider=provider) -def login_oauth_user(data, oauth_provider=None): +def login_oauth_user(data, provider=None): user = data["email"] if not frappe.db.exists("Profile", user): - create_oauth_user(data, oauth_provider) + create_oauth_user(data, provider) + + frappe.local._response = redirect("/") frappe.local.login_manager.user = user frappe.local.login_manager.post_login() -def create_oauth_user(data, oauth_provider): + # because of a GET request! + frappe.db.commit() + +def create_oauth_user(data, provider): if data.get("birthday"): - b = data.get("birthday").split("/") - data["birthday"] = b[2] + "-" + b[0] + "-" + b[1] + from frappe.utils.dateutils import parse_date + data["birthday"] = parse_date(data["birthday"]) + + if isinstance(data.get("location"), dict): + data["location"] = data.get("location").get("name") profile = frappe.bean({ "doctype":"Profile", - "first_name": data.get("first_name") or data.get("given_name"), + "first_name": data.get("first_name") or data.get("given_name") or data.get("name"), "last_name": data.get("last_name") or data.get("family_name"), "email": data["email"], "gender": data.get("gender"), "enabled": 1, "new_password": frappe.generate_hash(data["email"]), - "location": data.get("location", {}).get("name"), + "location": data.get("location"), "birth_date": data.get("birthday"), "user_type": "Website User", - "user_image": data.get("picture") + "user_image": data.get("picture") or data.get("avatar_url") }) - if oauth_provider=="facebook": + if provider=="facebook": profile.doc.fields.update({ "fb_username": data["username"], - "fb_userid": data["id"] + "fb_userid": data["id"], + "user_image": "https://graph.facebook.com/{username}/picture".format(username=data["username"]) }) - elif oauth_provider=="google": + elif provider=="google": profile.doc.google_userid = data["id"] + elif provider=="github": + profile.doc.github_userid = data["id"] + profile.doc.github_username = data["login"] + profile.ignore_permissions = True profile.get_controller().no_welcome_mail = True profile.insert() - -def get_fb_userid(fb_access_token): - import requests - response = requests.get("https://graph.facebook.com/me?access_token=" + fb_access_token) - if response.status_code==200: - print response.json() - return response.json().get("id") - else: - return frappe.AuthenticationError \ No newline at end of file diff --git a/frappe/website/doctype/blog_category/blog_category.py b/frappe/website/doctype/blog_category/blog_category.py index 8d97e47888..492ca04eb6 100644 --- a/frappe/website/doctype/blog_category/blog_category.py +++ b/frappe/website/doctype/blog_category/blog_category.py @@ -15,7 +15,7 @@ class DocType(WebsiteGenerator): self.doc.name = self.doc.category_name def get_page_title(self): - return self.doc.title + return self.doc.title or self.doc.name def on_update(self): WebsiteGenerator.on_update(self) diff --git a/frappe/website/render.py b/frappe/website/render.py index 3128fe0a7e..f02d7d9591 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -76,7 +76,8 @@ def build_page(path): return html def is_ajax(): - return frappe.get_request_header("X-Requested-With")=="XMLHttpRequest" + return (frappe.get_request_header("X-Requested-With")=="XMLHttpRequest" + if hasattr(frappe.local, "_response") else False) def resolve_path(path): if not path: diff --git a/frappe/website/sitemap.py b/frappe/website/sitemap.py index 5b1efb829f..a09b4acf03 100644 --- a/frappe/website/sitemap.py +++ b/frappe/website/sitemap.py @@ -73,5 +73,3 @@ def set_sidebar_items(sitemap_options, pathname, home_page): and t1.docname = t2.name order by t2.{sort_by} {sort_order}""".format(**website_template.fields), pathname, as_dict=True) - - print sitemap_options.children diff --git a/requirements.txt b/requirements.txt index 5c8b554743..2c8a0b93ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ chardet cssmin dropbox -oauth2client gunicorn httplib2 jinja2 @@ -20,3 +19,4 @@ werkzeug semantic_version lxml inlinestyler +rauth