mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
update tests with oauth confirmation
cross-user / service oauth tests must submit oauth confirmation form to complete authorization
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
|
"""Tests for service authentication"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
import copy
|
import copy
|
||||||
|
from functools import partial
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
@@ -24,7 +26,7 @@ from ..services.auth import _ExpiringDict, HubAuth, HubAuthenticated
|
|||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
from .mocking import public_url, public_host
|
from .mocking import public_url, public_host
|
||||||
from .test_api import add_user
|
from .test_api import add_user
|
||||||
from .utils import async_requests
|
from .utils import async_requests, AsyncSession
|
||||||
|
|
||||||
# mock for sending monotonic counter way into the future
|
# mock for sending monotonic counter way into the future
|
||||||
monotonic_future = mock.patch('time.monotonic', lambda : sys.maxsize)
|
monotonic_future = mock.patch('time.monotonic', lambda : sys.maxsize)
|
||||||
@@ -322,24 +324,29 @@ def test_hubauth_service_token(app, mockservice_url):
|
|||||||
def test_oauth_service(app, mockservice_url):
|
def test_oauth_service(app, mockservice_url):
|
||||||
service = mockservice_url
|
service = mockservice_url
|
||||||
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/?arg=x')
|
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/?arg=x')
|
||||||
# first request is only going to set login cookie
|
# first request is only going to login and get us to the oauth form page
|
||||||
s = requests.Session()
|
s = AsyncSession()
|
||||||
name = 'link'
|
name = 'link'
|
||||||
s.cookies = yield app.login_user(name)
|
s.cookies = yield app.login_user(name)
|
||||||
# run session.get in async_requests thread
|
|
||||||
s_get = lambda *args, **kwargs: async_requests.executor.submit(s.get, *args, **kwargs)
|
r = yield s.get(url)
|
||||||
r = yield s_get(url)
|
r.raise_for_status()
|
||||||
|
# we should be looking at the oauth confirmation page
|
||||||
|
assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize'
|
||||||
|
# verify oauth state cookie was set at some point
|
||||||
|
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
|
||||||
|
|
||||||
|
# submit the oauth form to complete authorization
|
||||||
|
r = yield s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url})
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.url == url
|
assert r.url == url
|
||||||
# verify oauth cookie is set
|
# verify oauth cookie is set
|
||||||
assert 'service-%s' % service.name in set(s.cookies.keys())
|
assert 'service-%s' % service.name in set(s.cookies.keys())
|
||||||
# verify oauth state cookie has been consumed
|
# verify oauth state cookie has been consumed
|
||||||
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
|
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
|
||||||
# verify oauth state cookie was set at some point
|
|
||||||
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
|
|
||||||
|
|
||||||
# second request should be authenticated
|
# second request should be authenticated, which means no redirects
|
||||||
r = yield s_get(url, allow_redirects=False)
|
r = yield s.get(url, allow_redirects=False)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
@@ -376,25 +383,23 @@ def test_oauth_cookie_collision(app, mockservice_url):
|
|||||||
service = mockservice_url
|
service = mockservice_url
|
||||||
url = url_path_join(public_url(app, mockservice_url), 'owhoami/')
|
url = url_path_join(public_url(app, mockservice_url), 'owhoami/')
|
||||||
print(url)
|
print(url)
|
||||||
s = requests.Session()
|
s = AsyncSession()
|
||||||
name = 'mypha'
|
name = 'mypha'
|
||||||
s.cookies = yield app.login_user(name)
|
s.cookies = yield app.login_user(name)
|
||||||
# run session.get in async_requests thread
|
|
||||||
s_get = lambda *args, **kwargs: async_requests.executor.submit(s.get, *args, **kwargs)
|
|
||||||
state_cookie_name = 'service-%s-oauth-state' % service.name
|
state_cookie_name = 'service-%s-oauth-state' % service.name
|
||||||
service_cookie_name = 'service-%s' % service.name
|
service_cookie_name = 'service-%s' % service.name
|
||||||
oauth_1 = yield s_get(url, allow_redirects=False)
|
oauth_1 = yield s.get(url)
|
||||||
print(oauth_1.headers)
|
print(oauth_1.headers)
|
||||||
print(oauth_1.cookies, oauth_1.url, url)
|
print(oauth_1.cookies, oauth_1.url, url)
|
||||||
assert state_cookie_name in s.cookies
|
assert state_cookie_name in s.cookies
|
||||||
state_cookies = [ s for s in s.cookies.keys() if s.startswith(state_cookie_name) ]
|
state_cookies = [ c for c in s.cookies.keys() if c.startswith(state_cookie_name) ]
|
||||||
# only one state cookie
|
# only one state cookie
|
||||||
assert state_cookies == [state_cookie_name]
|
assert state_cookies == [state_cookie_name]
|
||||||
state_1 = s.cookies[state_cookie_name]
|
state_1 = s.cookies[state_cookie_name]
|
||||||
|
|
||||||
# start second oauth login before finishing the first
|
# start second oauth login before finishing the first
|
||||||
oauth_2 = yield s_get(url, allow_redirects=False)
|
oauth_2 = yield s.get(url)
|
||||||
state_cookies = [ s for s in s.cookies.keys() if s.startswith(state_cookie_name) ]
|
state_cookies = [ c for c in s.cookies.keys() if c.startswith(state_cookie_name) ]
|
||||||
assert len(state_cookies) == 2
|
assert len(state_cookies) == 2
|
||||||
# get the random-suffix cookie name
|
# get the random-suffix cookie name
|
||||||
state_cookie_2 = sorted(state_cookies)[-1]
|
state_cookie_2 = sorted(state_cookies)[-1]
|
||||||
@@ -402,11 +407,14 @@ def test_oauth_cookie_collision(app, mockservice_url):
|
|||||||
assert s.cookies[state_cookie_name] == state_1
|
assert s.cookies[state_cookie_name] == state_1
|
||||||
|
|
||||||
# finish oauth 2
|
# finish oauth 2
|
||||||
url = oauth_2.headers['Location']
|
# submit the oauth form to complete authorization
|
||||||
if not urlparse(url).netloc:
|
r = yield s.post(
|
||||||
url = public_host(app) + url
|
oauth_2.url,
|
||||||
r = yield s_get(url)
|
data={'scopes': ['identify']},
|
||||||
|
headers={'Referer': oauth_2.url},
|
||||||
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
assert r.url == url
|
||||||
# after finishing, state cookie is cleared
|
# after finishing, state cookie is cleared
|
||||||
assert state_cookie_2 not in s.cookies
|
assert state_cookie_2 not in s.cookies
|
||||||
# service login cookie is set
|
# service login cookie is set
|
||||||
@@ -414,11 +422,14 @@ def test_oauth_cookie_collision(app, mockservice_url):
|
|||||||
service_cookie_2 = s.cookies[service_cookie_name]
|
service_cookie_2 = s.cookies[service_cookie_name]
|
||||||
|
|
||||||
# finish oauth 1
|
# finish oauth 1
|
||||||
url = oauth_1.headers['Location']
|
r = yield s.post(
|
||||||
if not urlparse(url).netloc:
|
oauth_1.url,
|
||||||
url = public_host(app) + url
|
data={'scopes': ['identify']},
|
||||||
r = yield s_get(url)
|
headers={'Referer': oauth_1.url},
|
||||||
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
assert r.url == url
|
||||||
|
|
||||||
# after finishing, state cookie is cleared (again)
|
# after finishing, state cookie is cleared (again)
|
||||||
assert state_cookie_name not in s.cookies
|
assert state_cookie_name not in s.cookies
|
||||||
# service login cookie is set (again, to a different value)
|
# service login cookie is set (again, to a different value)
|
||||||
@@ -443,7 +454,7 @@ def test_oauth_logout(app, mockservice_url):
|
|||||||
service_cookie_name = 'service-%s' % service.name
|
service_cookie_name = 'service-%s' % service.name
|
||||||
url = url_path_join(public_url(app, mockservice_url), 'owhoami/?foo=bar')
|
url = url_path_join(public_url(app, mockservice_url), 'owhoami/?foo=bar')
|
||||||
# first request is only going to set login cookie
|
# first request is only going to set login cookie
|
||||||
s = requests.Session()
|
s = AsyncSession()
|
||||||
name = 'propha'
|
name = 'propha'
|
||||||
app_user = add_user(app.db, app=app, name=name)
|
app_user = add_user(app.db, app=app, name=name)
|
||||||
def auth_tokens():
|
def auth_tokens():
|
||||||
@@ -458,13 +469,16 @@ def test_oauth_logout(app, mockservice_url):
|
|||||||
|
|
||||||
s.cookies = yield app.login_user(name)
|
s.cookies = yield app.login_user(name)
|
||||||
assert 'jupyterhub-session-id' in s.cookies
|
assert 'jupyterhub-session-id' in s.cookies
|
||||||
# run session.get in async_requests thread
|
r = yield s.get(url)
|
||||||
s_get = lambda *args, **kwargs: async_requests.executor.submit(s.get, *args, **kwargs)
|
r.raise_for_status()
|
||||||
r = yield s_get(url)
|
assert urlparse(r.url).path.endswith('oauth2/authorize')
|
||||||
|
# submit the oauth form to complete authorization
|
||||||
|
r = yield s.post(r.url, data={'scopes': ['identify']}, headers={'Referer': r.url})
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.url == url
|
assert r.url == url
|
||||||
|
|
||||||
# second request should be authenticated
|
# second request should be authenticated
|
||||||
r = yield s_get(url, allow_redirects=False)
|
r = yield s.get(url, allow_redirects=False)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
@@ -483,13 +497,13 @@ def test_oauth_logout(app, mockservice_url):
|
|||||||
assert len(auth_tokens()) == 1
|
assert len(auth_tokens()) == 1
|
||||||
|
|
||||||
# hit hub logout URL
|
# hit hub logout URL
|
||||||
r = yield s_get(public_url(app, path='hub/logout'))
|
r = yield s.get(public_url(app, path='hub/logout'))
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
# verify that all cookies other than the service cookie are cleared
|
# verify that all cookies other than the service cookie are cleared
|
||||||
assert list(s.cookies.keys()) == [service_cookie_name]
|
assert list(s.cookies.keys()) == [service_cookie_name]
|
||||||
# verify that clearing session id invalidates service cookie
|
# verify that clearing session id invalidates service cookie
|
||||||
# i.e. redirect back to login page
|
# i.e. redirect back to login page
|
||||||
r = yield s_get(url)
|
r = yield s.get(url)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.url.split('?')[0] == public_url(app, path='hub/login')
|
assert r.url.split('?')[0] == public_url(app, path='hub/login')
|
||||||
|
|
||||||
@@ -506,7 +520,7 @@ def test_oauth_logout(app, mockservice_url):
|
|||||||
# check that we got the old session id back
|
# check that we got the old session id back
|
||||||
assert session_id == s.cookies['jupyterhub-session-id']
|
assert session_id == s.cookies['jupyterhub-session-id']
|
||||||
|
|
||||||
r = yield s_get(url, allow_redirects=False)
|
r = yield s.get(url, allow_redirects=False)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
|
@@ -10,7 +10,7 @@ import jupyterhub
|
|||||||
from .mocking import StubSingleUserSpawner, public_url
|
from .mocking import StubSingleUserSpawner, public_url
|
||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
from .utils import async_requests
|
from .utils import async_requests, AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gen_test
|
@pytest.mark.gen_test
|
||||||
@@ -41,9 +41,20 @@ def test_singleuser_auth(app):
|
|||||||
r = yield async_requests.get(url_path_join(url, 'logout'), cookies=cookies)
|
r = yield async_requests.get(url_path_join(url, 'logout'), cookies=cookies)
|
||||||
assert len(r.cookies) == 0
|
assert len(r.cookies) == 0
|
||||||
|
|
||||||
# another user accessing should get 403, not redirect to login
|
# accessing another user's server hits the oauth confirmation page
|
||||||
cookies = yield app.login_user('burgess')
|
cookies = yield app.login_user('burgess')
|
||||||
r = yield async_requests.get(url, cookies=cookies)
|
s = AsyncSession()
|
||||||
|
s.cookies = cookies
|
||||||
|
r = yield s.get(url)
|
||||||
|
assert urlparse(r.url).path.endswith('/oauth2/authorize')
|
||||||
|
# submit the oauth form to complete authorization
|
||||||
|
r = yield s.post(
|
||||||
|
r.url,
|
||||||
|
data={'scopes': ['identify']},
|
||||||
|
headers={'Referer': r.url},
|
||||||
|
)
|
||||||
|
assert urlparse(r.url).path.rstrip('/').endswith('/user/nandy/tree')
|
||||||
|
# user isn't authorized, should raise 403
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
assert 'burgess' in r.text
|
assert 'burgess' in r.text
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import requests
|
|||||||
|
|
||||||
class _AsyncRequests:
|
class _AsyncRequests:
|
||||||
"""Wrapper around requests to return a Future from request methods
|
"""Wrapper around requests to return a Future from request methods
|
||||||
|
|
||||||
A single thread is allocated to avoid blocking the IOLoop thread.
|
A single thread is allocated to avoid blocking the IOLoop thread.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -16,3 +16,7 @@ class _AsyncRequests:
|
|||||||
# async_requests.get = requests.get returning a Future, etc.
|
# async_requests.get = requests.get returning a Future, etc.
|
||||||
async_requests = _AsyncRequests()
|
async_requests = _AsyncRequests()
|
||||||
|
|
||||||
|
class AsyncSession(requests.Session):
|
||||||
|
"""requests.Session object that runs in the background thread"""
|
||||||
|
def request(self, *args, **kwargs):
|
||||||
|
return async_requests.executor.submit(super().request, *args, **kwargs)
|
||||||
|
Reference in New Issue
Block a user