Compare commits

...

16 Commits
4.1.1 ... 4.1.4

Author SHA1 Message Date
Min RK
42191672ac Bump to 4.1.4 2024-03-30 09:58:35 +01:00
Min RK
669d8d7b65 Merge pull request #4764 from minrk/414
changelog for 4.1.4
2024-03-30 09:58:09 +01:00
Min RK
171026583c changelog for 4.1.4 2024-03-30 09:55:16 +01:00
Min RK
78a3dc5b01 Merge pull request #4759 from minrk/xsrf-no-navigate
avoid xsrf check on navigate GET requests
2024-03-30 09:53:09 +01:00
Min RK
21c37309a5 avoid xsrf check on navigate GET requests
sevices/auth prevents calling check_xsrf_cookie,
but if the Handler itself called it the newly strict check would still be applied

this ensures the check is actually allowed for navigate GET requests
2024-03-29 09:55:49 +01:00
Min RK
3d40be5890 Bump to 4.1.3 2024-03-26 10:07:04 +01:00
Min RK
ac72c60cb3 Merge pull request #4754 from minrk/413
changelog for 4.1.3
2024-03-26 10:06:38 +01:00
Min RK
92264696b1 changelog for 4.1.3 2024-03-26 09:44:07 +01:00
Min RK
f2b7b69c3e Merge pull request #4753 from minrk/server-xsrf-config
respect jupyter-server disable_check_xsrf setting
2024-03-26 09:42:54 +01:00
Min RK
e0f001271b respect jupyter-server disable_check_xsrf setting
allows global disable of xsrf checks in single-user servers
2024-03-26 08:55:15 +01:00
Min RK
d27e760677 Bump to 4.1.2 2024-03-25 20:47:05 +01:00
Min RK
3999556ed8 Merge pull request #4751 from minrk/cl-412
changelog for 4.1.2
2024-03-25 20:44:56 +01:00
Min RK
ff14797b9b changelog for 4.1.2 2024-03-25 20:39:47 +01:00
Min RK
f0cbec191e Merge pull request #4750 from minrk/fix-named-server
rework handling of multiple xsrf tokens
2024-03-25 20:36:15 +01:00
Min RK
87c2aebb5c avoid cycle on current_user in _set_xsrf_cookie
pass authenticated explicitly
2024-03-25 13:50:17 +01:00
Min RK
e0ea52af49 rework handling of multiple xsrf tokens
rather than attempting to clear multiple tokens (too complicated, breaks named servers)
look for and accept first valid token

have to do our own cookie parsing because existing cookie implementations only return a single value for each key
and default to selecting the _least_ likely to be correct, according to RFCs.

set updated xsrf cookie on login to avoid needing two requests to get the right cookie
2024-03-25 12:32:15 +01:00
11 changed files with 273 additions and 135 deletions

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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"""

View File

@@ -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"))

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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