Skip to content

Commit da696ff

Browse files
author
Jesse
authored
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.
1 parent ed654a7 commit da696ff

File tree

3 files changed

+100
-93
lines changed

3 files changed

+100
-93
lines changed

Diff for: redash/authentication/__init__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,13 @@ def logout_and_redirect_to_index():
243243

244244
def init_app(app):
245245
from redash.authentication import (
246-
google_oauth,
247246
saml_auth,
248247
remote_user_auth,
249248
ldap_auth,
250249
)
251250

251+
from redash.authentication.google_oauth import create_google_oauth_blueprint
252+
252253
login_manager.init_app(app)
253254
login_manager.anonymous_user = models.AnonymousUser
254255
login_manager.REMEMBER_COOKIE_DURATION = settings.REMEMBER_COOKIE_DURATION
@@ -259,8 +260,9 @@ def extend_session():
259260
app.permanent_session_lifetime = timedelta(seconds=settings.SESSION_EXPIRY_TIME)
260261

261262
from redash.security import csrf
262-
for auth in [google_oauth, saml_auth, remote_user_auth, ldap_auth]:
263-
blueprint = auth.blueprint
263+
264+
# Authlib's flask oauth client requires a Flask app to initialize
265+
for blueprint in [create_google_oauth_blueprint(app), saml_auth.blueprint, remote_user_auth.blueprint, ldap_auth.blueprint, ]:
264266
csrf.exempt(blueprint)
265267
app.register_blueprint(blueprint)
266268

Diff for: redash/authentication/google_oauth.py

+94-87
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import requests
33
from flask import redirect, url_for, Blueprint, flash, request, session
4-
from flask_oauthlib.client import OAuth
4+
55

66
from redash import models, settings
77
from redash.authentication import (
@@ -11,42 +11,7 @@
1111
)
1212
from redash.authentication.org_resolving import current_org
1313

14-
logger = logging.getLogger("google_oauth")
15-
16-
oauth = OAuth()
17-
blueprint = Blueprint("google_oauth", __name__)
18-
19-
20-
def google_remote_app():
21-
if "google" not in oauth.remote_apps:
22-
oauth.remote_app(
23-
"google",
24-
base_url="https://www.google.com/accounts/",
25-
authorize_url="https://accounts.google.com/o/oauth2/auth?prompt=select_account+consent",
26-
request_token_url=None,
27-
request_token_params={
28-
"scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
29-
},
30-
access_token_url="https://accounts.google.com/o/oauth2/token",
31-
access_token_method="POST",
32-
consumer_key=settings.GOOGLE_CLIENT_ID,
33-
consumer_secret=settings.GOOGLE_CLIENT_SECRET,
34-
)
35-
36-
return oauth.google
37-
38-
39-
def get_user_profile(access_token):
40-
headers = {"Authorization": "OAuth {}".format(access_token)}
41-
response = requests.get(
42-
"https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
43-
)
44-
45-
if response.status_code == 401:
46-
logger.warning("Failed getting user profile (response code 401).")
47-
return None
48-
49-
return response.json()
14+
from authlib.integrations.flask_client import OAuth
5015

5116

5217
def verify_profile(org, profile):
@@ -65,60 +30,102 @@ def verify_profile(org, profile):
6530
return False
6631

6732

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

36+
logger = logging.getLogger("google_oauth")
37+
blueprint = Blueprint("google_oauth", __name__)
7338

74-
@blueprint.route("/oauth/google", endpoint="authorize")
75-
def login():
76-
callback = url_for(".callback", _external=True)
77-
next_path = request.args.get(
78-
"next", url_for("redash.index", org_slug=session.get("org_slug"))
39+
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
40+
oauth = OAuth(app)
41+
oauth.register(
42+
name="google",
43+
server_metadata_url=CONF_URL,
44+
client_kwargs={"scope": "openid email profile"},
7945
)
80-
logger.debug("Callback url: %s", callback)
81-
logger.debug("Next is: %s", next_path)
82-
return google_remote_app().authorize(callback=callback, state=next_path)
83-
84-
85-
@blueprint.route("/oauth/google_callback", endpoint="callback")
86-
def authorized():
87-
resp = google_remote_app().authorized_response()
88-
access_token = resp["access_token"]
89-
90-
if access_token is None:
91-
logger.warning("Access token missing in call back request.")
92-
flash("Validation error. Please retry.")
93-
return redirect(url_for("redash.login"))
94-
95-
profile = get_user_profile(access_token)
96-
if profile is None:
97-
flash("Validation error. Please retry.")
98-
return redirect(url_for("redash.login"))
99-
100-
if "org_slug" in session:
101-
org = models.Organization.get_by_slug(session.pop("org_slug"))
102-
else:
103-
org = current_org
104-
105-
if not verify_profile(org, profile):
106-
logger.warning(
107-
"User tried to login with unauthorized domain name: %s (org: %s)",
108-
profile["email"],
109-
org,
46+
47+
def get_user_profile(access_token):
48+
headers = {"Authorization": "OAuth {}".format(access_token)}
49+
response = requests.get(
50+
"https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
11051
)
111-
flash("Your Google Apps account ({}) isn't allowed.".format(profile["email"]))
112-
return redirect(url_for("redash.login", org_slug=org.slug))
11352

114-
picture_url = "%s?sz=40" % profile["picture"]
115-
user = create_and_login_user(org, profile["name"], profile["email"], picture_url)
116-
if user is None:
117-
return logout_and_redirect_to_index()
53+
if response.status_code == 401:
54+
logger.warning("Failed getting user profile (response code 401).")
55+
return None
11856

119-
unsafe_next_path = request.args.get("state") or url_for(
120-
"redash.index", org_slug=org.slug
121-
)
122-
next_path = get_next_path(unsafe_next_path)
57+
return response.json()
58+
59+
@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
60+
def org_login(org_slug):
61+
session["org_slug"] = current_org.slug
62+
return redirect(url_for(".authorize", next=request.args.get("next", None)))
63+
64+
@blueprint.route("/oauth/google", endpoint="authorize")
65+
def login():
66+
67+
redirect_uri = url_for(".callback", _external=True)
68+
69+
next_path = request.args.get(
70+
"next", url_for("redash.index", org_slug=session.get("org_slug"))
71+
)
72+
logger.debug("Callback url: %s", redirect_uri)
73+
logger.debug("Next is: %s", next_path)
74+
75+
session["next_url"] = next_path
76+
77+
return oauth.google.authorize_redirect(redirect_uri)
78+
79+
@blueprint.route("/oauth/google_callback", endpoint="callback")
80+
def authorized():
81+
82+
logger.debug("Authorized user inbound")
83+
84+
resp = oauth.google.authorize_access_token()
85+
user = resp.get("userinfo")
86+
if user:
87+
session["user"] = user
88+
89+
access_token = resp["access_token"]
90+
91+
if access_token is None:
92+
logger.warning("Access token missing in call back request.")
93+
flash("Validation error. Please retry.")
94+
return redirect(url_for("redash.login"))
95+
96+
profile = get_user_profile(access_token)
97+
if profile is None:
98+
flash("Validation error. Please retry.")
99+
return redirect(url_for("redash.login"))
100+
101+
if "org_slug" in session:
102+
org = models.Organization.get_by_slug(session.pop("org_slug"))
103+
else:
104+
org = current_org
105+
106+
if not verify_profile(org, profile):
107+
logger.warning(
108+
"User tried to login with unauthorized domain name: %s (org: %s)",
109+
profile["email"],
110+
org,
111+
)
112+
flash(
113+
"Your Google Apps account ({}) isn't allowed.".format(profile["email"])
114+
)
115+
return redirect(url_for("redash.login", org_slug=org.slug))
116+
117+
picture_url = "%s?sz=40" % profile["picture"]
118+
user = create_and_login_user(
119+
org, profile["name"], profile["email"], picture_url
120+
)
121+
if user is None:
122+
return logout_and_redirect_to_index()
123+
124+
unsafe_next_path = session.get("next_url") or url_for(
125+
"redash.index", org_slug=org.slug
126+
)
127+
next_path = get_next_path(unsafe_next_path)
128+
129+
return redirect(next_path)
123130

124-
return redirect(next_path)
131+
return blueprint

Diff for: requirements.txt

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ httplib2==0.14.0
88
wtforms==2.2.1
99
Flask-RESTful==0.3.7
1010
Flask-Login==0.4.1
11-
Flask-OAuthLib==0.9.5
12-
# pin this until https://github.com/lepture/flask-oauthlib/pull/388 is released
13-
requests-oauthlib>=0.6.2,<1.2.0
1411
Flask-SQLAlchemy==2.4.1
1512
Flask-Migrate==2.5.2
1613
flask-mail==0.9.1
@@ -67,3 +64,4 @@ werkzeug==0.16.1
6764
# Uncomment the requirement for ldap3 if using ldap.
6865
# It is not included by default because of the GPL license conflict.
6966
# ldap3==2.2.4
67+
Authlib==0.15.5

0 commit comments

Comments
 (0)