mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
42191672ac | ||
![]() |
669d8d7b65 | ||
![]() |
171026583c | ||
![]() |
78a3dc5b01 | ||
![]() |
21c37309a5 | ||
![]() |
3d40be5890 | ||
![]() |
ac72c60cb3 | ||
![]() |
92264696b1 | ||
![]() |
f2b7b69c3e | ||
![]() |
e0f001271b | ||
![]() |
d27e760677 | ||
![]() |
3999556ed8 | ||
![]() |
ff14797b9b | ||
![]() |
f0cbec191e | ||
![]() |
87c2aebb5c | ||
![]() |
e0ea52af49 |
@@ -6,7 +6,7 @@ info:
|
||||
description: The REST API for JupyterHub
|
||||
license:
|
||||
name: BSD-3-Clause
|
||||
version: 4.1.1
|
||||
version: 4.1.4
|
||||
servers:
|
||||
- url: /hub/api
|
||||
security:
|
||||
|
@@ -10,6 +10,59 @@ command line for details.
|
||||
|
||||
## 4.1
|
||||
|
||||
### 4.1.4 - 2024-03-30
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.3...4.1.4))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- avoid xsrf check on navigate GET requests [#4759](https://github.com/jupyterhub/jupyterhub/pull/4759) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2024-03-26&to=2024-03-30&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-26..2024-03-30&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-26..2024-03-30&type=Issues))
|
||||
|
||||
### 4.1.3 - 2024-03-26
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.2...4.1.3))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- respect jupyter-server disable_check_xsrf setting [#4753](https://github.com/jupyterhub/jupyterhub/pull/4753) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2024-03-25&to=2024-03-26&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-25..2024-03-26&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-25..2024-03-26&type=Issues))
|
||||
|
||||
### 4.1.2 - 2024-03-25
|
||||
|
||||
4.1.2 fixes a regression in 4.1.0 affecting named servers.
|
||||
|
||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/4.1.1...4.1.2))
|
||||
|
||||
#### Bugs fixed
|
||||
|
||||
- rework handling of multiple xsrf tokens [#4750](https://github.com/jupyterhub/jupyterhub/pull/4750) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio))
|
||||
|
||||
#### Contributors to this release
|
||||
|
||||
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
||||
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
||||
|
||||
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2024-03-23&to=2024-03-25&type=c))
|
||||
|
||||
@consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2024-03-23..2024-03-25&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2024-03-23..2024-03-25&type=Issues))
|
||||
|
||||
### 4.1.1 - 2024-03-23
|
||||
|
||||
4.1.1 fixes a compatibility regression in 4.1.0 for some extensions,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (4, 1, 1, "", "")
|
||||
version_info = (4, 1, 4, "", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -10,11 +10,9 @@ in both Hub and single-user code
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from tornado import web
|
||||
from tornado.httputil import format_timestamp
|
||||
from tornado.log import app_log
|
||||
|
||||
|
||||
@@ -60,41 +58,76 @@ def _create_signed_value_urlsafe(handler, name, value):
|
||||
return base64.urlsafe_b64encode(signed_value).rstrip(b"=")
|
||||
|
||||
|
||||
def _clear_invalid_xsrf_cookie(handler, cookie_path):
|
||||
def _get_xsrf_token_cookie(handler):
|
||||
"""
|
||||
Clear invalid XSRF cookie
|
||||
Get the _valid_ XSRF token and id from Cookie
|
||||
|
||||
This may an old XSRF token, or one set on / by another application.
|
||||
Because we cannot trust browsers or tornado to give us the more specific cookie,
|
||||
try to clear _both_ on / and on our prefix,
|
||||
then reload the page.
|
||||
Returns (xsrf_token, xsrf_id) found in Cookies header.
|
||||
|
||||
multiple xsrf cookies may be set on multiple paths;
|
||||
|
||||
RFC 6265 states that they should be in order of more specific path to less,
|
||||
but ALSO states that servers should never rely on order.
|
||||
|
||||
Tornado (6.4) and stdlib (3.12) SimpleCookie explicitly use the _last_ value,
|
||||
which means the cookie with the _least_ specific prefix will be used if more than one is present.
|
||||
|
||||
Because we sign values, we can get the first valid cookie and not worry about order too much.
|
||||
|
||||
This is simplified from tornado's HTTPRequest.cookies property
|
||||
only looking for a single cookie.
|
||||
"""
|
||||
|
||||
expired = format_timestamp(datetime.now(timezone.utc) - timedelta(days=366))
|
||||
cookie = SimpleCookie()
|
||||
cookie["_xsrf"] = ""
|
||||
morsel = cookie["_xsrf"]
|
||||
morsel["expires"] = expired
|
||||
morsel["path"] = "/"
|
||||
# use Set-Cookie directly,
|
||||
# because tornado's set_cookie and clear_cookie use a single _dict_,
|
||||
# so we can't clear a cookie on multiple paths and then set it
|
||||
handler.add_header("Set-Cookie", morsel.OutputString(None))
|
||||
if cookie_path != "/":
|
||||
# clear it multiple times!
|
||||
morsel["path"] = cookie_path
|
||||
handler.add_header("Set-Cookie", morsel.OutputString(None))
|
||||
if "Cookie" not in handler.request.headers:
|
||||
return (None, None)
|
||||
|
||||
if (
|
||||
handler.request.method.lower() == "get"
|
||||
and handler.request.headers.get("Sec-Fetch-Mode", "navigate") == "navigate"
|
||||
):
|
||||
# reload current page because any subsequent set_cookie
|
||||
# will cancel the clearing of the cookie
|
||||
# this only makes sense on GET requests
|
||||
handler.redirect(handler.request.uri)
|
||||
# halt any other processing of the request
|
||||
raise web.Finish()
|
||||
for chunk in handler.request.headers["Cookie"].split(";"):
|
||||
key = chunk.partition("=")[0].strip()
|
||||
if key != "_xsrf":
|
||||
# we are only looking for the _xsrf cookie
|
||||
# ignore everything else
|
||||
continue
|
||||
|
||||
# use stdlib parsing to handle quotes, validation, etc.
|
||||
try:
|
||||
xsrf_token = SimpleCookie(chunk)[key].value.encode("ascii")
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
|
||||
xsrf_token_id = _get_signed_value_urlsafe(handler, "_xsrf", xsrf_token)
|
||||
|
||||
if xsrf_token_id:
|
||||
# only return if we found a _valid_ xsrf cookie
|
||||
# otherwise, keep looking
|
||||
return (xsrf_token, xsrf_token_id)
|
||||
# no valid token found found
|
||||
return (None, None)
|
||||
|
||||
|
||||
def _set_xsrf_cookie(handler, xsrf_id, *, cookie_path="", authenticated=None):
|
||||
"""Set xsrf token cookie"""
|
||||
xsrf_token = _create_signed_value_urlsafe(handler, "_xsrf", xsrf_id)
|
||||
xsrf_cookie_kwargs = {}
|
||||
xsrf_cookie_kwargs.update(handler.settings.get('xsrf_cookie_kwargs', {}))
|
||||
xsrf_cookie_kwargs.setdefault("path", cookie_path)
|
||||
if authenticated is None:
|
||||
try:
|
||||
current_user = handler.current_user
|
||||
except Exception:
|
||||
authenticated = False
|
||||
else:
|
||||
authenticated = bool(current_user)
|
||||
if not authenticated:
|
||||
# limit anonymous xsrf cookies to one hour
|
||||
xsrf_cookie_kwargs.pop("expires", None)
|
||||
xsrf_cookie_kwargs.pop("expires_days", None)
|
||||
xsrf_cookie_kwargs["max_age"] = 3600
|
||||
app_log.info(
|
||||
"Setting new xsrf cookie for %r %r",
|
||||
xsrf_id,
|
||||
xsrf_cookie_kwargs,
|
||||
)
|
||||
handler.set_cookie("_xsrf", xsrf_token, **xsrf_cookie_kwargs)
|
||||
|
||||
|
||||
def get_xsrf_token(handler, cookie_path=""):
|
||||
@@ -110,23 +143,8 @@ def get_xsrf_token(handler, cookie_path=""):
|
||||
|
||||
_set_cookie = False
|
||||
# the raw cookie is the token
|
||||
xsrf_token = xsrf_cookie = handler.get_cookie("_xsrf")
|
||||
if xsrf_token:
|
||||
try:
|
||||
xsrf_token = xsrf_token.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
xsrf_token = None
|
||||
|
||||
xsrf_id_cookie = _get_signed_value_urlsafe(handler, "_xsrf", xsrf_token)
|
||||
if xsrf_cookie and not xsrf_id_cookie:
|
||||
# we have a cookie, but it's invalid!
|
||||
# handle possibility of _xsrf being set multiple times,
|
||||
# e.g. on / and on /hub/
|
||||
# this will reload the page if it's a GET request
|
||||
app_log.warning(
|
||||
"Attempting to clear invalid _xsrf cookie %r", xsrf_cookie[:4] + "..."
|
||||
)
|
||||
_clear_invalid_xsrf_cookie(handler, cookie_path)
|
||||
xsrf_token, xsrf_id_cookie = _get_xsrf_token_cookie(handler)
|
||||
cookie_token = xsrf_token
|
||||
|
||||
# check the decoded, signed value for validity
|
||||
xsrf_id = handler._xsrf_token_id
|
||||
@@ -146,30 +164,49 @@ def get_xsrf_token(handler, cookie_path=""):
|
||||
_set_cookie = (
|
||||
handler.request.headers.get("Sec-Fetch-Mode", "navigate") == "navigate"
|
||||
)
|
||||
if xsrf_id_cookie and not _set_cookie:
|
||||
# if we aren't setting a cookie here but we got one,
|
||||
# this means things probably aren't going to work
|
||||
app_log.warning(
|
||||
"Not accepting incorrect xsrf token id in cookie on %s",
|
||||
handler.request.path,
|
||||
)
|
||||
|
||||
if _set_cookie:
|
||||
xsrf_cookie_kwargs = {}
|
||||
xsrf_cookie_kwargs.update(handler.settings.get('xsrf_cookie_kwargs', {}))
|
||||
xsrf_cookie_kwargs.setdefault("path", cookie_path)
|
||||
if not handler.current_user:
|
||||
# limit anonymous xsrf cookies to one hour
|
||||
xsrf_cookie_kwargs.pop("expires", None)
|
||||
xsrf_cookie_kwargs.pop("expires_days", None)
|
||||
xsrf_cookie_kwargs["max_age"] = 3600
|
||||
app_log.info(
|
||||
"Setting new xsrf cookie for %r %r",
|
||||
xsrf_id,
|
||||
xsrf_cookie_kwargs,
|
||||
)
|
||||
handler.set_cookie("_xsrf", xsrf_token, **xsrf_cookie_kwargs)
|
||||
_set_xsrf_cookie(handler, xsrf_id, cookie_path=cookie_path)
|
||||
handler._xsrf_token = xsrf_token
|
||||
return xsrf_token
|
||||
|
||||
|
||||
def _needs_check_xsrf(handler):
|
||||
"""Does the given cookie-authenticated request need to check xsrf?"""
|
||||
|
||||
if getattr(handler, "_token_authenticated", False):
|
||||
return False
|
||||
|
||||
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "unspecified")
|
||||
if fetch_mode in {"websocket", "no-cors"} or (
|
||||
fetch_mode in {"navigate", "unspecified"}
|
||||
and handler.request.method.lower() in {"get", "head", "options"}
|
||||
):
|
||||
# no xsrf check needed for regular page views or no-cors
|
||||
# or websockets after allow_websocket_cookie_auth passes
|
||||
if fetch_mode == "unspecified":
|
||||
app_log.warning(
|
||||
f"Skipping XSRF check for insecure request {handler.request.method} {handler.request.path}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def check_xsrf_cookie(handler):
|
||||
"""Check that xsrf cookie matches xsrf token in request"""
|
||||
# overrides tornado's implementation
|
||||
# because we changed what a correct value should be in xsrf_token
|
||||
if not _needs_check_xsrf(handler):
|
||||
# don't require XSRF for regular page views
|
||||
return
|
||||
|
||||
token = (
|
||||
handler.get_argument("_xsrf", None)
|
||||
|
@@ -24,7 +24,12 @@ from tornado.log import app_log
|
||||
from tornado.web import RequestHandler, addslash
|
||||
|
||||
from .. import __version__, orm, roles, scopes
|
||||
from .._xsrf_utils import _anonymous_xsrf_id, check_xsrf_cookie, get_xsrf_token
|
||||
from .._xsrf_utils import (
|
||||
_anonymous_xsrf_id,
|
||||
_set_xsrf_cookie,
|
||||
check_xsrf_cookie,
|
||||
get_xsrf_token,
|
||||
)
|
||||
from ..metrics import (
|
||||
PROXY_ADD_DURATION_SECONDS,
|
||||
PROXY_DELETE_DURATION_SECONDS,
|
||||
@@ -730,6 +735,13 @@ class BaseHandler(RequestHandler):
|
||||
if not self.get_current_user_cookie():
|
||||
self.set_hub_cookie(user)
|
||||
|
||||
# make sure xsrf cookie is updated
|
||||
# this avoids needing a second request to set the right xsrf cookie
|
||||
self._jupyterhub_user = user
|
||||
_set_xsrf_cookie(
|
||||
self, self._xsrf_token_id, cookie_path=self.hub.base_url, authenticated=True
|
||||
)
|
||||
|
||||
def authenticate(self, data):
|
||||
return maybe_future(self.authenticator.get_authenticated_user(self, data))
|
||||
|
||||
|
@@ -45,6 +45,7 @@ from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||
from tornado.httputil import url_concat
|
||||
from tornado.log import app_log
|
||||
from tornado.web import HTTPError, RequestHandler
|
||||
from tornado.websocket import WebSocketHandler
|
||||
from traitlets import (
|
||||
Any,
|
||||
Bool,
|
||||
@@ -59,7 +60,13 @@ from traitlets import (
|
||||
)
|
||||
from traitlets.config import SingletonConfigurable
|
||||
|
||||
from .._xsrf_utils import _anonymous_xsrf_id, check_xsrf_cookie, get_xsrf_token
|
||||
from .._xsrf_utils import (
|
||||
_anonymous_xsrf_id,
|
||||
_needs_check_xsrf,
|
||||
_set_xsrf_cookie,
|
||||
check_xsrf_cookie,
|
||||
get_xsrf_token,
|
||||
)
|
||||
from ..scopes import _intersect_expanded_scopes
|
||||
from ..utils import _bool_env, get_browser_protocol, url_path_join
|
||||
|
||||
@@ -800,6 +807,10 @@ class HubAuth(SingletonConfigurable):
|
||||
if not hasattr(self, 'set_cookie'):
|
||||
# only HubOAuth can persist cookies
|
||||
return
|
||||
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "navigate")
|
||||
if isinstance(handler, WebSocketHandler) or fetch_mode != "navigate":
|
||||
# don't do this on websockets or non-navigate requests
|
||||
return
|
||||
self.log.info(
|
||||
"Storing token from url in cookie for %s",
|
||||
handler.request.remote_ip,
|
||||
@@ -851,6 +862,8 @@ class HubOAuth(HubAuth):
|
||||
|
||||
def _get_token_cookie(self, handler):
|
||||
"""Base class doesn't store tokens in cookies"""
|
||||
if hasattr(handler, "_hub_auth_token_cookie"):
|
||||
return handler._hub_auth_token_cookie
|
||||
|
||||
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "unset")
|
||||
if fetch_mode == "websocket" and not self.allow_websocket_cookie_auth:
|
||||
@@ -919,7 +932,9 @@ class HubOAuth(HubAuth):
|
||||
|
||||
Applies JupyterHub check_xsrf_cookie if not token authenticated
|
||||
"""
|
||||
if getattr(handler, '_token_authenticated', False):
|
||||
if getattr(handler, '_token_authenticated', False) or handler.settings.get(
|
||||
"disable_check_xsrf", False
|
||||
):
|
||||
return
|
||||
check_xsrf_cookie(handler)
|
||||
|
||||
@@ -932,38 +947,18 @@ class HubOAuth(HubAuth):
|
||||
kwargs["secure"] = True
|
||||
return handler.clear_cookie(cookie_name, **kwargs)
|
||||
|
||||
def _needs_check_xsrf(self, handler):
|
||||
"""Does the given cookie-authenticated request need to check xsrf?"""
|
||||
if getattr(handler, "_token_authenticated", False):
|
||||
return False
|
||||
|
||||
fetch_mode = handler.request.headers.get("Sec-Fetch-Mode", "unspecified")
|
||||
if fetch_mode in {"websocket", "no-cors"} or (
|
||||
fetch_mode in {"navigate", "unspecified"}
|
||||
and handler.request.method.lower() in {"get", "head", "options"}
|
||||
):
|
||||
# no xsrf check needed for regular page views or no-cors
|
||||
# or websockets after allow_websocket_cookie_auth passes
|
||||
if fetch_mode == "unspecified":
|
||||
self.log.warning(
|
||||
f"Skipping XSRF check for insecure request {handler.request.method} {handler.request.path}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def _get_user_cookie(self, handler):
|
||||
# check xsrf if needed
|
||||
token = self._get_token_cookie(handler)
|
||||
session_id = self.get_session_id(handler)
|
||||
if token and self._needs_check_xsrf(handler):
|
||||
if token and _needs_check_xsrf(handler):
|
||||
# call handler.check_xsrf_cookie instead of self.check_xsrf_cookie
|
||||
# to allow subclass overrides
|
||||
try:
|
||||
handler.check_xsrf_cookie()
|
||||
except HTTPError as e:
|
||||
self.log.error(
|
||||
f"Not accepting cookie auth on {handler.request.method} {handler.request.path}: {e}"
|
||||
self.log.debug(
|
||||
f"Not accepting cookie auth on {handler.request.method} {handler.request.path}: {e.log_message}"
|
||||
)
|
||||
# don't proceed with cookie auth unless xsrf is okay
|
||||
# don't raise either, because that makes a mess
|
||||
@@ -1187,6 +1182,15 @@ class HubOAuth(HubAuth):
|
||||
kwargs,
|
||||
)
|
||||
handler.set_secure_cookie(self.cookie_name, access_token, **kwargs)
|
||||
# set updated xsrf token cookie,
|
||||
# which changes after login
|
||||
handler._hub_auth_token_cookie = access_token
|
||||
_set_xsrf_cookie(
|
||||
handler,
|
||||
handler._xsrf_token_id,
|
||||
cookie_path=self.base_url,
|
||||
authenticated=True,
|
||||
)
|
||||
|
||||
def clear_cookie(self, handler):
|
||||
"""Clear the OAuth cookie"""
|
||||
|
@@ -12,6 +12,7 @@ from tornado.escape import url_escape
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from jupyterhub import orm, roles, scopes
|
||||
from jupyterhub.tests.test_named_servers import named_servers # noqa
|
||||
from jupyterhub.tests.utils import async_requests, public_host, public_url, ujoin
|
||||
from jupyterhub.utils import url_escape_path, url_path_join
|
||||
|
||||
@@ -1127,6 +1128,7 @@ async def test_start_stop_server_on_admin_page(
|
||||
"fresh",
|
||||
"invalid",
|
||||
"valid-prefix-invalid-root",
|
||||
"valid-prefix-invalid-other-prefix",
|
||||
],
|
||||
)
|
||||
async def test_login_xsrf_initial_cookies(app, browser, case, username):
|
||||
@@ -1136,6 +1138,7 @@ async def test_login_xsrf_initial_cookies(app, browser, case, username):
|
||||
"""
|
||||
hub_root = public_host(app)
|
||||
hub_url = url_path_join(public_host(app), app.hub.base_url)
|
||||
hub_parent = hub_url.rstrip("/").rsplit("/", 1)[0] + "/"
|
||||
login_url = url_path_join(
|
||||
hub_url, url_concat("login", {"next": url_path_join(app.base_url, "/hub/home")})
|
||||
)
|
||||
@@ -1145,7 +1148,11 @@ async def test_login_xsrf_initial_cookies(app, browser, case, username):
|
||||
await browser.context.add_cookies(
|
||||
[{"name": "_xsrf", "value": "invalid-hub-prefix", "url": hub_url}]
|
||||
)
|
||||
elif case == "valid-prefix-invalid-root":
|
||||
elif case.startswith("valid-prefix"):
|
||||
if "invalid-root" in case:
|
||||
invalid_url = hub_root
|
||||
else:
|
||||
invalid_url = hub_parent
|
||||
await browser.goto(login_url)
|
||||
# first visit sets valid xsrf cookie
|
||||
cookies = await browser.context.cookies()
|
||||
@@ -1157,7 +1164,7 @@ async def test_login_xsrf_initial_cookies(app, browser, case, username):
|
||||
# currently, this test assumes the observed behavior,
|
||||
# which is that the invalid cookie on `/` has _higher_ priority
|
||||
await browser.context.add_cookies(
|
||||
[{"name": "_xsrf", "value": "invalid-root", "url": hub_root}]
|
||||
[{"name": "_xsrf", "value": "invalid-root", "url": invalid_url}]
|
||||
)
|
||||
cookies = await browser.context.cookies()
|
||||
assert len(cookies) == 2
|
||||
@@ -1190,7 +1197,9 @@ def _cookie_dict(cookie_list):
|
||||
return cookie_dict
|
||||
|
||||
|
||||
async def test_singleuser_xsrf(app, browser, user, create_user_with_scopes, full_spawn):
|
||||
async def test_singleuser_xsrf(
|
||||
app, browser, user, create_user_with_scopes, full_spawn, named_servers # noqa: F811
|
||||
):
|
||||
# full login process, checking XSRF handling
|
||||
# start two servers
|
||||
target_user = user
|
||||
@@ -1311,13 +1320,14 @@ async def test_singleuser_xsrf(app, browser, user, create_user_with_scopes, full
|
||||
|
||||
# check that server page can still connect to its own kernels
|
||||
token = target_user.new_api_token(scopes=["access:servers!user"])
|
||||
url = url_path_join(public_url(app, target_user), "/api/kernels")
|
||||
|
||||
async def test_kernel(kernels_url):
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
r = await async_requests.post(url, headers=headers)
|
||||
r = await async_requests.post(kernels_url, headers=headers)
|
||||
r.raise_for_status()
|
||||
kernel = r.json()
|
||||
kernel_id = kernel["id"]
|
||||
kernel_url = url_path_join(url, kernel_id)
|
||||
kernel_url = url_path_join(kernels_url, kernel_id)
|
||||
kernel_ws_url = "ws" + url_path_join(kernel_url, "channels")[4:]
|
||||
try:
|
||||
result = await browser.evaluate(
|
||||
@@ -1341,3 +1351,30 @@ async def test_singleuser_xsrf(app, browser, user, create_user_with_scopes, full
|
||||
r = await async_requests.delete(kernel_url, headers=headers)
|
||||
r.raise_for_status()
|
||||
assert result == "ok"
|
||||
|
||||
kernels_url = url_path_join(public_url(app, target_user), "/api/kernels")
|
||||
await test_kernel(kernels_url)
|
||||
|
||||
# final check: make sure named servers work.
|
||||
# first, visit spawn page to launch server,
|
||||
# will issue cookies, etc.
|
||||
server_name = "named"
|
||||
url = url_path_join(
|
||||
public_host(app),
|
||||
url_path_join(app.base_url, f"hub/spawn/{browser_user.name}/{server_name}"),
|
||||
)
|
||||
await browser.goto(url)
|
||||
await expect(browser).to_have_url(
|
||||
re.compile(rf".*/user/{browser_user.name}/{server_name}/.*")
|
||||
)
|
||||
# from named server URL, make sure we can talk to a kernel
|
||||
token = browser_user.new_api_token(scopes=["access:servers!user"])
|
||||
# named-server URL
|
||||
kernels_url = url_path_join(
|
||||
public_url(app, browser_user), server_name, "api/kernels"
|
||||
)
|
||||
await test_kernel(kernels_url)
|
||||
# go back to user's own page, test again
|
||||
# make sure we didn't break anything
|
||||
await browser.goto(public_url(app, browser_user))
|
||||
await test_kernel(url_path_join(public_url(app, browser_user), "api/kernels"))
|
||||
|
@@ -448,8 +448,6 @@ def create_user_with_scopes(app, create_temp_role):
|
||||
return app.users[orm_user.id]
|
||||
|
||||
yield temp_user_creator
|
||||
for user in temp_users:
|
||||
app.users.delete(user)
|
||||
|
||||
|
||||
@fixture
|
||||
|
@@ -157,7 +157,7 @@ async def test_permission_error_messages(app, user, auth, expected_message):
|
||||
params["_xsrf"] = cookies["_xsrf"]
|
||||
if auth == "cookie_xsrf_mismatch":
|
||||
params["_xsrf"] = "somethingelse"
|
||||
|
||||
headers['Sec-Fetch-Mode'] = 'cors'
|
||||
r = await async_requests.get(url, **kwargs)
|
||||
assert r.status_code == 403
|
||||
response = r.json()
|
||||
|
@@ -528,7 +528,7 @@ async def test_oauth_cookie_collision(app, mockservice_url, create_user_with_sco
|
||||
print(url)
|
||||
s = AsyncSession()
|
||||
name = 'mypha'
|
||||
user = create_user_with_scopes("access:services", name=name)
|
||||
create_user_with_scopes("access:services", name=name)
|
||||
s.cookies = await app.login_user(name)
|
||||
state_cookie_name = 'service-%s-oauth-state' % service.name
|
||||
service_cookie_name = 'service-%s' % service.name
|
||||
@@ -551,10 +551,9 @@ async def test_oauth_cookie_collision(app, mockservice_url, create_user_with_sco
|
||||
assert s.cookies[state_cookie_name] == state_1
|
||||
|
||||
# finish oauth 2
|
||||
hub_xsrf = s.cookies.get("_xsrf", path=app.hub.base_url)
|
||||
# submit the oauth form to complete authorization
|
||||
r = await s.post(
|
||||
oauth_2.url, data={'scopes': ['identify'], "_xsrf": s.cookies["_xsrf"]}
|
||||
)
|
||||
r = await s.post(oauth_2.url, data={'scopes': ['identify'], "_xsrf": hub_xsrf})
|
||||
r.raise_for_status()
|
||||
assert r.url == url
|
||||
# after finishing, state cookie is cleared
|
||||
@@ -564,9 +563,7 @@ async def test_oauth_cookie_collision(app, mockservice_url, create_user_with_sco
|
||||
service_cookie_2 = s.cookies[service_cookie_name]
|
||||
|
||||
# finish oauth 1
|
||||
r = await s.post(
|
||||
oauth_1.url, data={'scopes': ['identify'], "_xsrf": s.cookies["_xsrf"]}
|
||||
)
|
||||
r = await s.post(oauth_1.url, data={'scopes': ['identify'], "_xsrf": hub_xsrf})
|
||||
r.raise_for_status()
|
||||
assert r.url == url
|
||||
|
||||
@@ -635,7 +632,7 @@ async def test_oauth_logout(app, mockservice_url, create_user_with_scopes):
|
||||
r = await s.get(public_url(app, path='hub/logout'))
|
||||
r.raise_for_status()
|
||||
# verify that all cookies other than the service cookie are cleared
|
||||
assert sorted(s.cookies.keys()) == ["_xsrf", service_cookie_name]
|
||||
assert sorted(set(s.cookies.keys())) == ["_xsrf", service_cookie_name]
|
||||
# verify that clearing session id invalidates service cookie
|
||||
# i.e. redirect back to login page
|
||||
r = await s.get(url)
|
||||
|
@@ -43,7 +43,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "4.1.1"
|
||||
current = "4.1.4"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
Reference in New Issue
Block a user