157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# MIT License. See license.txt
|
|
|
|
from __future__ import annotations
|
|
|
|
import jingrow
|
|
from frappe import _
|
|
from google.auth.transport.requests import Request
|
|
from google.oauth2 import id_token
|
|
from google_auth_oauthlib.flow import Flow
|
|
from oauthlib.oauth2 import AccessDeniedError
|
|
|
|
from press.utils import log_error
|
|
from press.utils.telemetry import capture
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def login(product=None):
|
|
flow = google_oauth_flow()
|
|
authorization_url, state = flow.authorization_url()
|
|
minutes = 5
|
|
payload = {"state": state}
|
|
if product:
|
|
payload["product"] = product
|
|
frappe.cache().set_value(f"google_oauth_flow:{state}", payload, expires_in_sec=minutes * 60)
|
|
return authorization_url
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def callback(code=None, state=None): # noqa: C901
|
|
cached_key = f"google_oauth_flow:{state}"
|
|
payload = frappe.cache().get_value(cached_key)
|
|
if not payload:
|
|
return invalid_login()
|
|
|
|
product = payload.get("product")
|
|
product_trial = frappe.db.get_value("Product Trial", product, ["name"], as_dict=1) if product else None
|
|
|
|
def _redirect_to_login_on_failed_authentication():
|
|
frappe.local.response.type = "redirect"
|
|
if product_trial:
|
|
frappe.local.response.location = f"/dashboard/login?product={product_trial.name}"
|
|
else:
|
|
frappe.local.response.location = "/dashboard/login"
|
|
|
|
try:
|
|
flow = google_oauth_flow()
|
|
flow.fetch_token(authorization_response=frappe.request.url)
|
|
except AccessDeniedError:
|
|
_redirect_to_login_on_failed_authentication()
|
|
return None
|
|
except Exception as e:
|
|
log_error("Google Login failed", data=e)
|
|
_redirect_to_login_on_failed_authentication()
|
|
return None
|
|
|
|
# authenticated
|
|
frappe.cache().delete_value(cached_key)
|
|
|
|
# id_info
|
|
token_request = Request()
|
|
google_credentials = get_google_credentials()
|
|
id_info = id_token.verify_oauth2_token(
|
|
id_token=flow.credentials._id_token,
|
|
request=token_request,
|
|
audience=google_credentials["web"]["client_id"],
|
|
)
|
|
|
|
email = id_info.get("email")
|
|
|
|
team_name, team_enabled = frappe.db.get_value("Team", {"user": email}, ["name", "enabled"]) or [0, 0]
|
|
|
|
if team_name and not team_enabled:
|
|
frappe.throw(_("Account {0} has been deactivated").format(email))
|
|
return None
|
|
|
|
# if team exitst and oauth is not using in saas login/signup flow
|
|
if team_name and not product_trial:
|
|
has_2fa = frappe.db.get_value("User 2FA", {"user": email, "enabled": 1})
|
|
if has_2fa:
|
|
# redirect to 2fa page
|
|
frappe.respond_as_web_page(
|
|
_("Two-Factor Authentication Required"),
|
|
_(
|
|
"Google OAuth login doesn't support 2FA. Please login using your email and verification code / password."
|
|
),
|
|
primary_action="/dashboard/login",
|
|
primary_label=_("Login with Email"),
|
|
)
|
|
return None
|
|
|
|
# login to existing account
|
|
frappe.local.login_manager.login_as(email)
|
|
frappe.local.response.type = "redirect"
|
|
frappe.local.response.location = "/dashboard"
|
|
return None
|
|
|
|
# create account request
|
|
account_request = frappe.get_pg(
|
|
doctype="Account Request",
|
|
email=email,
|
|
first_name=id_info.get("given_name"),
|
|
last_name=id_info.get("family_name"),
|
|
role="Press Admin",
|
|
oauth_signup=True,
|
|
product_trial=product_trial.name if product_trial else None,
|
|
)
|
|
account_request.insert(ignore_permissions=True)
|
|
frappe.db.commit()
|
|
|
|
if product_trial:
|
|
# dummy event so that the stat in funnel won't break
|
|
capture("otp_verified", "fc_product_trial", account_request.name)
|
|
|
|
if team_name and product_trial:
|
|
frappe.local.login_manager.login_as(email)
|
|
frappe.local.response.type = "redirect"
|
|
frappe.local.response.location = f"/dashboard/create-site/{product_trial.name}/setup"
|
|
else:
|
|
# create/setup account
|
|
frappe.local.response.type = "redirect"
|
|
frappe.local.response.location = account_request.get_verification_url()
|
|
return None
|
|
|
|
|
|
def invalid_login():
|
|
frappe.local.response["http_status_code"] = 401
|
|
return "Invalid state parameter. The session timed out. Please try again or contact Frappe Cloud support at https://jcloud.jingrow.com/support"
|
|
|
|
|
|
def google_oauth_flow():
|
|
google_credentials = get_google_credentials()
|
|
redirect_uri = google_credentials["web"].get("redirect_uris")[0]
|
|
redirect_uri = redirect_uri.replace("press.api.oauth.callback", "press.api.google.callback")
|
|
return Flow.from_client_config(
|
|
client_config=google_credentials,
|
|
scopes=[
|
|
"https://www.googleapis.com/auth/userinfo.profile",
|
|
"openid",
|
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
],
|
|
redirect_uri=redirect_uri,
|
|
)
|
|
|
|
|
|
def get_google_credentials():
|
|
if frappe.local.dev_server:
|
|
import os
|
|
|
|
# flow.fetch_token doesn't work with http, so this is needed for local development
|
|
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
|
|
config = frappe.conf.get("google_credentials")
|
|
if not config:
|
|
frappe.throw("google_credentials not found in site_config.json")
|
|
return config
|