seitime-frappe/frappe/utils/oauth.py
Gavin D'souza 3446026555 chore: Update header: license.txt => LICENSE
The license.txt file has been replaced with LICENSE for quite a while
now. INAL but it didn't seem accurate to say "hey, checkout license.txt
although there's no such file". Apart from this, there were
inconsistencies in the headers altogether...this change brings
consistency.
2021-09-03 12:02:59 +05:30

312 lines
9.6 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
import frappe.utils
import json, jwt
import base64
from frappe import _
from frappe.utils.password import get_decrypted_password
class SignupDisabledError(frappe.PermissionError): pass
def get_oauth2_providers():
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": 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)
if provider.api_endpoint_args:
out[provider.name]["api_endpoint_args"] = json.loads(provider.api_endpoint_args)
return out
def get_oauth_keys(provider):
"""get client_id and client_secret from database or conf"""
# try conf
keys = frappe.conf.get("{provider}_login".format(provider=provider))
if not keys:
# try database
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"],
"client_secret": keys["client_secret"]
}
def get_oauth2_authorize_url(provider, redirect_to):
flow = get_oauth2_flow(provider)
state = { "site": frappe.utils.get_url(), "token": frappe.generate_hash(), "redirect_to": redirect_to }
# relative to absolute url
data = {
"redirect_uri": get_redirect_uri(provider),
"state": base64.b64encode(bytes(json.dumps(state).encode("utf-8")))
}
oauth2_providers = get_oauth2_providers()
# 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)
oauth2_providers = get_oauth2_providers()
# 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):
keys = frappe.conf.get("{provider}_login".format(provider=provider))
if keys and keys.get("redirect_uri"):
# this should be a fully qualified redirect uri
return keys["redirect_uri"]
else:
oauth2_providers = get_oauth2_providers()
redirect_uri = oauth2_providers[provider]["redirect_uri"]
# this uses the site's url + the relative redirect uri
return frappe.utils.get_url(redirect_uri)
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 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()
args = {
"data": {
"code": code,
"redirect_uri": get_redirect_uri(provider),
"grant_type": "authorization_code"
}
}
if decoder:
args["decoder"] = decoder
session = flow.get_auth_session(**args)
if id_token:
parsed_access = json.loads(session.access_token_response.text)
token = parsed_access['id_token']
info = jwt.decode(token, flow.client_secret, options={"verify_signature": 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 not (info.get("email_verified") or info.get("email")):
frappe.throw(_("Email not verified with {0}").format(provider.title()))
return info
def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False):
# NOTE: This could lead to security issue as the signed in user can type any email address in complete_signup
# if email_id and key:
# data = json.loads(frappe.db.get_temp(key))
# # What if data is missing because of an invalid key
# data["email"] = email_id
#
# elif not (data.get("email") and get_first_name(data)) and not frappe.db.exists("User", data.get("email")):
# # ask for user email
# key = frappe.db.set_temp(json.dumps(data))
# frappe.db.commit()
# frappe.local.response["type"] = "redirect"
# frappe.local.response["location"] = "/complete_signup?key=" + key
# return
# json.loads data and state
if isinstance(data, str):
data = json.loads(data)
if isinstance(state, str):
state = base64.b64decode(state)
state = json.loads(state.decode("utf-8"))
if not (state and state["token"]):
frappe.respond_as_web_page(_("Invalid Request"), _("Token is missing"), http_status_code=417)
return
user = get_email(data)
if not user:
frappe.respond_as_web_page(_("Invalid Request"), _("Please ensure that your profile has an email address"))
return
try:
if update_oauth_user(user, data, provider) is False:
return
except SignupDisabledError:
return frappe.respond_as_web_page("Signup is Disabled", "Sorry. Signup from Website is disabled.",
success=False, http_status_code=403)
frappe.local.login_manager.user = user
frappe.local.login_manager.post_login()
# because of a GET request!
frappe.db.commit()
if frappe.utils.cint(generate_login_token):
login_token = frappe.generate_hash(length=32)
frappe.cache().set_value("login_token:{0}".format(login_token), frappe.local.session.sid, expires_in_sec=120)
frappe.response["login_token"] = login_token
else:
redirect_to = state.get("redirect_to")
redirect_post_login(
desk_user=frappe.local.response.get('message') == 'Logged In',
redirect_to=redirect_to,
provider=provider
)
def update_oauth_user(user, data, provider):
if isinstance(data.get("location"), dict):
data["location"] = data.get("location").get("name")
save = False
if not frappe.db.exists("User", user):
# is signup disabled?
if frappe.utils.cint(frappe.db.get_single_value("Website Settings", "disable_signup")):
raise SignupDisabledError
save = True
user = frappe.new_doc("User")
gender = data.get("gender", "").title()
if gender and not frappe.db.exists("Gender", gender):
doc = frappe.new_doc("Gender", {"gender": gender})
doc.insert(ignore_permissions=True)
user.update({
"doctype": "User",
"first_name": get_first_name(data),
"last_name": get_last_name(data),
"email": get_email(data),
"gender": gender,
"enabled": 1,
"new_password": frappe.generate_hash(get_email(data)),
"location": data.get("location"),
"user_type": "Website User",
"user_image": data.get("picture") or data.get("avatar_url")
})
else:
user = frappe.get_doc("User", user)
if not user.enabled:
frappe.respond_as_web_page(_('Not Allowed'), _('User {0} is disabled').format(user.email))
return False
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({
"user_image": "https://graph.facebook.com/{id}/picture".format(id=data["id"])
})
elif provider=="google" and not user.get_social_login_userid(provider):
save = True
user.set_social_login_userid(provider, userid=data["id"])
elif provider=="github" and not user.get_social_login_userid(provider):
save = True
user.set_social_login_userid(provider, userid=data["id"], username=data.get("login"))
elif provider=="frappe" and not user.get_social_login_userid(provider):
save = True
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"])
elif provider=="salesforce" and not user.get_social_login_userid(provider):
save = True
user.set_social_login_userid(provider, userid="/".join(data["sub"].split("/")[-2:]))
elif not user.get_social_login_userid(provider):
save = True
user_id_property = frappe.db.get_value("Social Login Key", provider, "user_id_property") or "sub"
user.set_social_login_userid(provider, userid=data[user_id_property])
if save:
user.flags.ignore_permissions = True
user.flags.no_welcome_mail = True
# set default signup role as per Portal Settings
default_role = frappe.db.get_single_value("Portal Settings", "default_role")
if default_role:
user.add_roles(default_role)
user.save()
def get_first_name(data):
return data.get("first_name") or data.get("given_name") or data.get("name")
def get_last_name(data):
return data.get("last_name") or data.get("family_name")
def get_email(data):
return data.get("email") or data.get("upn") or data.get("unique_name")
def redirect_post_login(desk_user, redirect_to=None, provider=None):
# redirect!
frappe.local.response["type"] = "redirect"
if not redirect_to:
# the #desktop is added to prevent a facebook redirect bug
desk_uri = "/app/workspace" if provider == 'facebook' else '/app'
redirect_to = desk_uri if desk_user else "/me"
redirect_to = frappe.utils.get_url(redirect_to)
frappe.local.response["location"] = redirect_to