Skip to content
Permalink
Browse files Browse the repository at this point in the history
Merge pull request from GHSA-vhc7-w7r8-8m34
* WIP: break the flask_oauthlib behavior

* Refactor google-oauth to use cryptographic state.

* Clean up comments

* Fix: tests didn't pass because of the scope issues.

Moved outside the create_blueprint method because this does not depend
on the Authlib object.

* Apply Arik's fixes. Tests pass.
  • Loading branch information
susodapop committed Nov 23, 2021
1 parent ed654a7 commit da696ff
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 93 deletions.
8 changes: 5 additions & 3 deletions redash/authentication/__init__.py
Expand Up @@ -243,12 +243,13 @@ def logout_and_redirect_to_index():

def init_app(app):
from redash.authentication import (
google_oauth,
saml_auth,
remote_user_auth,
ldap_auth,
)

from redash.authentication.google_oauth import create_google_oauth_blueprint

login_manager.init_app(app)
login_manager.anonymous_user = models.AnonymousUser
login_manager.REMEMBER_COOKIE_DURATION = settings.REMEMBER_COOKIE_DURATION
Expand All @@ -259,8 +260,9 @@ def extend_session():
app.permanent_session_lifetime = timedelta(seconds=settings.SESSION_EXPIRY_TIME)

from redash.security import csrf
for auth in [google_oauth, saml_auth, remote_user_auth, ldap_auth]:
blueprint = auth.blueprint

# Authlib's flask oauth client requires a Flask app to initialize
for blueprint in [create_google_oauth_blueprint(app), saml_auth.blueprint, remote_user_auth.blueprint, ldap_auth.blueprint, ]:
csrf.exempt(blueprint)
app.register_blueprint(blueprint)

Expand Down
181 changes: 94 additions & 87 deletions redash/authentication/google_oauth.py
@@ -1,7 +1,7 @@
import logging
import requests
from flask import redirect, url_for, Blueprint, flash, request, session
from flask_oauthlib.client import OAuth


from redash import models, settings
from redash.authentication import (
Expand All @@ -11,42 +11,7 @@
)
from redash.authentication.org_resolving import current_org

logger = logging.getLogger("google_oauth")

oauth = OAuth()
blueprint = Blueprint("google_oauth", __name__)


def google_remote_app():
if "google" not in oauth.remote_apps:
oauth.remote_app(
"google",
base_url="https://www.google.com/accounts/",
authorize_url="https://accounts.google.com/o/oauth2/auth?prompt=select_account+consent",
request_token_url=None,
request_token_params={
"scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
},
access_token_url="https://accounts.google.com/o/oauth2/token",
access_token_method="POST",
consumer_key=settings.GOOGLE_CLIENT_ID,
consumer_secret=settings.GOOGLE_CLIENT_SECRET,
)

return oauth.google


def get_user_profile(access_token):
headers = {"Authorization": "OAuth {}".format(access_token)}
response = requests.get(
"https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
)

if response.status_code == 401:
logger.warning("Failed getting user profile (response code 401).")
return None

return response.json()
from authlib.integrations.flask_client import OAuth


def verify_profile(org, profile):
Expand All @@ -65,60 +30,102 @@ def verify_profile(org, profile):
return False


@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
def org_login(org_slug):
session["org_slug"] = current_org.slug
return redirect(url_for(".authorize", next=request.args.get("next", None)))
def create_google_oauth_blueprint(app):
oauth = OAuth(app)

logger = logging.getLogger("google_oauth")
blueprint = Blueprint("google_oauth", __name__)

@blueprint.route("/oauth/google", endpoint="authorize")
def login():
callback = url_for(".callback", _external=True)
next_path = request.args.get(
"next", url_for("redash.index", org_slug=session.get("org_slug"))
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
oauth = OAuth(app)
oauth.register(
name="google",
server_metadata_url=CONF_URL,
client_kwargs={"scope": "openid email profile"},
)
logger.debug("Callback url: %s", callback)
logger.debug("Next is: %s", next_path)
return google_remote_app().authorize(callback=callback, state=next_path)


@blueprint.route("/oauth/google_callback", endpoint="callback")
def authorized():
resp = google_remote_app().authorized_response()
access_token = resp["access_token"]

if access_token is None:
logger.warning("Access token missing in call back request.")
flash("Validation error. Please retry.")
return redirect(url_for("redash.login"))

profile = get_user_profile(access_token)
if profile is None:
flash("Validation error. Please retry.")
return redirect(url_for("redash.login"))

if "org_slug" in session:
org = models.Organization.get_by_slug(session.pop("org_slug"))
else:
org = current_org

if not verify_profile(org, profile):
logger.warning(
"User tried to login with unauthorized domain name: %s (org: %s)",
profile["email"],
org,

def get_user_profile(access_token):
headers = {"Authorization": "OAuth {}".format(access_token)}
response = requests.get(
"https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
)
flash("Your Google Apps account ({}) isn't allowed.".format(profile["email"]))
return redirect(url_for("redash.login", org_slug=org.slug))

picture_url = "%s?sz=40" % profile["picture"]
user = create_and_login_user(org, profile["name"], profile["email"], picture_url)
if user is None:
return logout_and_redirect_to_index()
if response.status_code == 401:
logger.warning("Failed getting user profile (response code 401).")
return None

unsafe_next_path = request.args.get("state") or url_for(
"redash.index", org_slug=org.slug
)
next_path = get_next_path(unsafe_next_path)
return response.json()

@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
def org_login(org_slug):
session["org_slug"] = current_org.slug
return redirect(url_for(".authorize", next=request.args.get("next", None)))

@blueprint.route("/oauth/google", endpoint="authorize")
def login():

redirect_uri = url_for(".callback", _external=True)

next_path = request.args.get(
"next", url_for("redash.index", org_slug=session.get("org_slug"))
)
logger.debug("Callback url: %s", redirect_uri)
logger.debug("Next is: %s", next_path)

session["next_url"] = next_path

return oauth.google.authorize_redirect(redirect_uri)

@blueprint.route("/oauth/google_callback", endpoint="callback")
def authorized():

logger.debug("Authorized user inbound")

resp = oauth.google.authorize_access_token()
user = resp.get("userinfo")
if user:
session["user"] = user

access_token = resp["access_token"]

if access_token is None:
logger.warning("Access token missing in call back request.")
flash("Validation error. Please retry.")
return redirect(url_for("redash.login"))

profile = get_user_profile(access_token)
if profile is None:
flash("Validation error. Please retry.")
return redirect(url_for("redash.login"))

if "org_slug" in session:
org = models.Organization.get_by_slug(session.pop("org_slug"))
else:
org = current_org

if not verify_profile(org, profile):
logger.warning(
"User tried to login with unauthorized domain name: %s (org: %s)",
profile["email"],
org,
)
flash(
"Your Google Apps account ({}) isn't allowed.".format(profile["email"])
)
return redirect(url_for("redash.login", org_slug=org.slug))

picture_url = "%s?sz=40" % profile["picture"]
user = create_and_login_user(
org, profile["name"], profile["email"], picture_url
)
if user is None:
return logout_and_redirect_to_index()

unsafe_next_path = session.get("next_url") or url_for(
"redash.index", org_slug=org.slug
)
next_path = get_next_path(unsafe_next_path)

return redirect(next_path)

return redirect(next_path)
return blueprint
4 changes: 1 addition & 3 deletions requirements.txt
Expand Up @@ -8,9 +8,6 @@ httplib2==0.14.0
wtforms==2.2.1
Flask-RESTful==0.3.7
Flask-Login==0.4.1
Flask-OAuthLib==0.9.5
# pin this until https://github.com/lepture/flask-oauthlib/pull/388 is released
requests-oauthlib>=0.6.2,<1.2.0
Flask-SQLAlchemy==2.4.1
Flask-Migrate==2.5.2
flask-mail==0.9.1
Expand Down Expand Up @@ -67,3 +64,4 @@ werkzeug==0.16.1
# Uncomment the requirement for ldap3 if using ldap.
# It is not included by default because of the GPL license conflict.
# ldap3==2.2.4
Authlib==0.15.5

0 comments on commit da696ff

Please sign in to comment.