mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-09 11:03:00 +00:00
better error messages on oauth errors
This commit is contained in:
@@ -104,7 +104,7 @@ class CookieAPIHandler(APIHandler):
|
|||||||
self.write(json.dumps(self.user_model(user)))
|
self.write(json.dumps(self.user_model(user)))
|
||||||
|
|
||||||
|
|
||||||
class OAuthHandler(BaseHandler):
|
class OAuthHandler:
|
||||||
def extract_oauth_params(self):
|
def extract_oauth_params(self):
|
||||||
"""extract oauthlib params from a request
|
"""extract oauthlib params from a request
|
||||||
|
|
||||||
@@ -230,13 +230,11 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
|
|||||||
|
|
||||||
# Errors that should be shown to the user on the provider website
|
# Errors that should be shown to the user on the provider website
|
||||||
except oauth2.FatalClientError as e:
|
except oauth2.FatalClientError as e:
|
||||||
# TODO: human error page
|
raise web.HTTPError(e.status_code, e.description)
|
||||||
raise
|
|
||||||
# return response_from_error(e)
|
|
||||||
|
|
||||||
# Errors embedded in the redirect URI back to the client
|
# Errors embedded in the redirect URI back to the client
|
||||||
except oauth2.OAuth2Error as e:
|
except oauth2.OAuth2Error as e:
|
||||||
self.log.error("oauth error: %s" % e)
|
self.log.error("OAuth error: %s", e.description)
|
||||||
self.redirect(e.in_uri(e.redirect_uri))
|
self.redirect(e.in_uri(e.redirect_uri))
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
@@ -259,20 +257,22 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
|
|||||||
uri, http_method, body, headers, scopes, credentials,
|
uri, http_method, body, headers, scopes, credentials,
|
||||||
)
|
)
|
||||||
except oauth2.FatalClientError as e:
|
except oauth2.FatalClientError as e:
|
||||||
# TODO: human error page
|
raise web.HTTPError(e.status_code, e.description)
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
self.send_oauth_response(headers, body, status)
|
self.send_oauth_response(headers, body, status)
|
||||||
|
|
||||||
|
|
||||||
class OAuthTokenHandler(OAuthHandler, APIHandler):
|
class OAuthTokenHandler(OAuthHandler, APIHandler):
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
uri, http_method, body, headers = self.extract_oauth_params()
|
uri, http_method, body, headers = self.extract_oauth_params()
|
||||||
credentials = {}
|
credentials = {}
|
||||||
|
|
||||||
|
try:
|
||||||
headers, body, status = self.oauth_provider.create_token_response(
|
headers, body, status = self.oauth_provider.create_token_response(
|
||||||
uri, http_method, body, headers, credentials)
|
uri, http_method, body, headers, credentials)
|
||||||
|
except oauth2.FatalClientError as e:
|
||||||
|
raise web.HTTPError(e.status_code, e.description)
|
||||||
|
else:
|
||||||
self.send_oauth_response(headers, body, status)
|
self.send_oauth_response(headers, body, status)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ implements https://oauthlib.readthedocs.io/en/latest/oauth2/server.html
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from oauthlib.oauth2 import RequestValidator, WebApplicationServer
|
from oauthlib.oauth2 import RequestValidator, WebApplicationServer
|
||||||
|
|
||||||
@@ -98,22 +99,15 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
Method is used by:
|
Method is used by:
|
||||||
- Authorization Code Grant (during token request)
|
- Authorization Code Grant (during token request)
|
||||||
"""
|
"""
|
||||||
|
# TODO: record redirect_uri used during oauth
|
||||||
|
# if we ever support multiple destinations
|
||||||
app_log.debug("confirm_redirect_uri: client_id=%s, redirect_uri=%s",
|
app_log.debug("confirm_redirect_uri: client_id=%s, redirect_uri=%s",
|
||||||
client_id, redirect_uri,
|
client_id, redirect_uri,
|
||||||
)
|
)
|
||||||
orm_client = (
|
if redirect_uri == client.redirect_uri:
|
||||||
self.db
|
|
||||||
.query(orm.OAuthClient)
|
|
||||||
.filter_by(identifier=client_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if orm_client is None:
|
|
||||||
app_log.warning("No such oauth client %s", client_id)
|
|
||||||
return False
|
|
||||||
if redirect_uri == orm_client.redirect_uri:
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
app_log.warning("Redirect uri %s != %s", redirect_uri, orm_client.redirect_uri)
|
app_log.warning("Redirect uri %s != %s", redirect_uri, client.redirect_uri)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
|
||||||
@@ -465,7 +459,7 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
- Client Credentials Grant
|
- Client Credentials Grant
|
||||||
- Refresh Token Grant
|
- Refresh Token Grant
|
||||||
"""
|
"""
|
||||||
return True
|
return grant_type == 'authorization_code'
|
||||||
|
|
||||||
def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
|
def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
|
||||||
"""Ensure client is authorized to redirect to the redirect_uri requested.
|
"""Ensure client is authorized to redirect to the redirect_uri requested.
|
||||||
@@ -479,8 +473,34 @@ class JupyterHubRequestValidator(RequestValidator):
|
|||||||
- Authorization Code Grant
|
- Authorization Code Grant
|
||||||
- Implicit Grant
|
- Implicit Grant
|
||||||
"""
|
"""
|
||||||
|
app_log.debug("validate_redirect_uri: client_id=%s, redirect_uri=%s",
|
||||||
|
client_id, redirect_uri,
|
||||||
|
)
|
||||||
|
orm_client = (
|
||||||
|
self.db
|
||||||
|
.query(orm.OAuthClient)
|
||||||
|
.filter_by(identifier=client_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if orm_client is None:
|
||||||
|
app_log.warning("No such oauth client %s", client_id)
|
||||||
|
return False
|
||||||
|
if '://' in redirect_uri and '://' not in orm_client.redirect_uri:
|
||||||
|
# default internal "/path/only" redirect uri
|
||||||
|
# confirm it matches our Host header and protocol of Referer
|
||||||
|
expected = "{}://{}{}".format(
|
||||||
|
urlparse(request.headers.get('Referer', '')).scheme,
|
||||||
|
request.headers.get('Host', '[missing Host]'),
|
||||||
|
orm_client.redirect_uri,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
expected = orm_client.redirect_uri
|
||||||
|
|
||||||
|
if redirect_uri == expected:
|
||||||
return True
|
return True
|
||||||
raise NotImplementedError('Subclasses must implement this method.')
|
else:
|
||||||
|
app_log.warning("Redirect uri %s != %s", redirect_uri, orm_client.redirect_uri)
|
||||||
|
return False
|
||||||
|
|
||||||
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
|
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
|
||||||
"""Ensure the Bearer token is valid and authorized access to scopes.
|
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||||
|
@@ -302,7 +302,15 @@ class HubAuth(SingletonConfigurable):
|
|||||||
elif r.status_code >= 400:
|
elif r.status_code >= 400:
|
||||||
app_log.warning("Failed to check authorization: [%i] %s", r.status_code, r.reason)
|
app_log.warning("Failed to check authorization: [%i] %s", r.status_code, r.reason)
|
||||||
app_log.warning(r.text)
|
app_log.warning(r.text)
|
||||||
raise HTTPError(500, "Failed to check authorization")
|
msg = "Failed to check authorization"
|
||||||
|
# pass on error_description from oauth failure
|
||||||
|
try:
|
||||||
|
description = r.json().get("error_description")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msg += ": " + description
|
||||||
|
raise HTTPError(500, msg)
|
||||||
else:
|
else:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
@@ -847,6 +855,11 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
|
|||||||
|
|
||||||
@coroutine
|
@coroutine
|
||||||
def get(self):
|
def get(self):
|
||||||
|
error = self.get_argument("error", False)
|
||||||
|
if error:
|
||||||
|
msg = self.get_argument("error_description", error)
|
||||||
|
raise HTTPError(400, "Error in oauth: %s" % msg)
|
||||||
|
|
||||||
code = self.get_argument("code", False)
|
code = self.get_argument("code", False)
|
||||||
if not code:
|
if not code:
|
||||||
raise HTTPError(400, "oauth callback made without a token")
|
raise HTTPError(400, "oauth callback made without a token")
|
||||||
|
@@ -573,6 +573,21 @@ def test_announcements(app, announcements):
|
|||||||
assert_announcement("logout", r.text)
|
assert_announcement("logout", r.text)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"redirect_uri=/noexist",
|
||||||
|
"redirect_uri=ok&client_id=nosuchthing",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@pytest.mark.gen_test
|
||||||
|
def test_bad_oauth_get(app, params):
|
||||||
|
cookies = yield app.login_user("authorizer")
|
||||||
|
r = yield get_page("hub/api/oauth2/authorize?" + params, app, hub=False, cookies=cookies)
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.gen_test
|
||||||
def test_server_not_running_api_request(app):
|
def test_server_not_running_api_request(app):
|
||||||
cookies = yield app.login_user("bees")
|
cookies = yield app.login_user("bees")
|
||||||
|
Reference in New Issue
Block a user