# Copyright (c) 2023, Jingrow Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt from __future__ import annotations import jingrow from jingrow 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 @jingrow.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 jingrow.cache().set_value(f"google_oauth_flow:{state}", payload, expires_in_sec=minutes * 60) return authorization_url @jingrow.whitelist(allow_guest=True) def callback(code=None, state=None): # noqa: C901 cached_key = f"google_oauth_flow:{state}" payload = jingrow.cache().get_value(cached_key) if not payload: return invalid_login() product = payload.get("product") product_trial = jingrow.db.get_value("Product Trial", product, ["name"], as_dict=1) if product else None def _redirect_to_login_on_failed_authentication(): jingrow.local.response.type = "redirect" if product_trial: jingrow.local.response.location = f"/dashboard/login?product={product_trial.name}" else: jingrow.local.response.location = "/dashboard/login" try: flow = google_oauth_flow() flow.fetch_token(authorization_response=jingrow.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 jingrow.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 = jingrow.db.get_value("Team", {"user": email}, ["name", "enabled"]) or [0, 0] if team_name and not team_enabled: jingrow.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 = jingrow.db.get_value("User 2FA", {"user": email, "enabled": 1}) if has_2fa: # redirect to 2fa page jingrow.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 jingrow.local.login_manager.login_as(email) jingrow.local.response.type = "redirect" jingrow.local.response.location = "/dashboard" return None # create account request account_request = jingrow.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) jingrow.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: jingrow.local.login_manager.login_as(email) jingrow.local.response.type = "redirect" jingrow.local.response.location = f"/dashboard/create-site/{product_trial.name}/setup" else: # create/setup account jingrow.local.response.type = "redirect" jingrow.local.response.location = account_request.get_verification_url() return None def invalid_login(): jingrow.local.response["http_status_code"] = 401 return "Invalid state parameter. The session timed out. Please try again or contact Jingrow 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 jingrow.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 = jingrow.conf.get("google_credentials") if not config: jingrow.throw("google_credentials not found in site_config.json") return config