jcloude/press/api/google.py
2025-12-23 19:17:16 +08:00

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 frappe
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_doc(
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://frappecloud.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