mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-12 12:33:02 +00:00
get rid of cookie tokens
use single cookie_id, since cookies themselves are already unique via `set_secure_cookie` resetting cookie_id effectively logs out all browser sessions for a given user
This commit is contained in:
@@ -26,15 +26,11 @@ class CookieAPIHandler(APIHandler):
|
|||||||
@token_authenticated
|
@token_authenticated
|
||||||
def get(self, cookie_name):
|
def get(self, cookie_name):
|
||||||
cookie_value = self.request.body
|
cookie_value = self.request.body
|
||||||
btoken = self.get_secure_cookie(cookie_name, cookie_value)
|
user = self._user_for_cookie(cookie_name, cookie_value)
|
||||||
if not btoken:
|
if user is None:
|
||||||
raise web.HTTPError(404)
|
|
||||||
token = btoken.decode('utf8', 'replace')
|
|
||||||
orm_token = orm.CookieToken.find(self.db, token)
|
|
||||||
if orm_token is None:
|
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
self.write(json.dumps({
|
self.write(json.dumps({
|
||||||
'user' : orm_token.user.name,
|
'user' : user.name,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
default_handlers = [
|
default_handlers = [
|
||||||
|
@@ -81,19 +81,24 @@ class BaseHandler(RequestHandler):
|
|||||||
user.last_activity = datetime.utcnow()
|
user.last_activity = datetime.utcnow()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def _user_for_cookie(self, cookie_name, cookie_value=None):
|
||||||
|
"""Get the User for a given cookie, if there is one"""
|
||||||
|
cookie_id = self.get_secure_cookie(cookie_name, cookie_value)
|
||||||
|
if cookie_id is None:
|
||||||
|
return
|
||||||
|
cookie_id = cookie_id.decode('utf8', 'replace')
|
||||||
|
return self.db.query(orm.User).filter(orm.User.cookie_id==cookie_id).first()
|
||||||
|
|
||||||
def get_current_user_cookie(self):
|
def get_current_user_cookie(self):
|
||||||
"""get_current_user from a cookie token"""
|
"""get_current_user from a cookie token"""
|
||||||
btoken = self.get_secure_cookie(self.hub.server.cookie_name)
|
user = self._user_for_cookie(self.hub.server.cookie_name)
|
||||||
if btoken:
|
if user:
|
||||||
token = btoken.decode('utf8', 'replace')
|
return user
|
||||||
cookie_token = orm.CookieToken.find(self.db, token)
|
else:
|
||||||
if cookie_token:
|
# don't log the token itself
|
||||||
return cookie_token.user
|
self.log.warn("Invalid cookie token")
|
||||||
else:
|
# have cookie, but it's not valid. Clear it and start over.
|
||||||
# don't log the token itself
|
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
|
||||||
self.log.warn("Invalid cookie token")
|
|
||||||
# have cookie, but it's not valid. Clear it and start over.
|
|
||||||
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
|
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
"""get current username"""
|
"""get current username"""
|
||||||
@@ -128,19 +133,17 @@ class BaseHandler(RequestHandler):
|
|||||||
"""Set login cookies for the Hub and single-user server."""
|
"""Set login cookies for the Hub and single-user server."""
|
||||||
# create and set a new cookie token for the single-user server
|
# create and set a new cookie token for the single-user server
|
||||||
if user.server:
|
if user.server:
|
||||||
cookie_token = user.new_cookie_token()
|
|
||||||
self.set_secure_cookie(
|
self.set_secure_cookie(
|
||||||
user.server.cookie_name,
|
user.server.cookie_name,
|
||||||
cookie_token,
|
user.cookie_id,
|
||||||
path=user.server.base_url,
|
path=user.server.base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create and set a new cookie token for the hub
|
# create and set a new cookie token for the hub
|
||||||
if not self.get_current_user_cookie():
|
if not self.get_current_user_cookie():
|
||||||
cookie_token = user.new_cookie_token()
|
|
||||||
self.set_secure_cookie(
|
self.set_secure_cookie(
|
||||||
self.hub.server.cookie_name,
|
self.hub.server.cookie_name,
|
||||||
cookie_token,
|
user.cookie_id,
|
||||||
path=self.hub.server.base_url)
|
path=self.hub.server.base_url)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
|
@@ -227,9 +227,11 @@ class User(Base):
|
|||||||
and multiple tokens used for authorization.
|
and multiple tokens used for authorization.
|
||||||
|
|
||||||
API tokens grant access to the Hub's REST API.
|
API tokens grant access to the Hub's REST API.
|
||||||
These are used by single-user servers to authenticate requests.
|
These are used by single-user servers to authenticate requests,
|
||||||
|
and external services to manipulate the Hub.
|
||||||
|
|
||||||
Cookie tokens are used to authenticate browser sessions.
|
Cookies are set with a single ID.
|
||||||
|
Resetting the Cookie ID invalidates all cookies, forcing user to login again.
|
||||||
|
|
||||||
A `state` column contains a JSON dict,
|
A `state` column contains a JSON dict,
|
||||||
used for restoring state of a Spawner.
|
used for restoring state of a Spawner.
|
||||||
@@ -244,7 +246,7 @@ class User(Base):
|
|||||||
last_activity = Column(DateTime, default=datetime.utcnow)
|
last_activity = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
api_tokens = relationship("APIToken", backref="user")
|
api_tokens = relationship("APIToken", backref="user")
|
||||||
cookie_tokens = relationship("CookieToken", backref="user")
|
cookie_id = Column(Unicode, default=new_token)
|
||||||
state = Column(JSONDict)
|
state = Column(JSONDict)
|
||||||
spawner = None
|
spawner = None
|
||||||
|
|
||||||
@@ -262,25 +264,17 @@ class User(Base):
|
|||||||
name=self.name,
|
name=self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _new_token(self, cls):
|
def new_api_token(self):
|
||||||
"""Create a new API or Cookie token"""
|
"""Create a new API token"""
|
||||||
assert self.id is not None
|
assert self.id is not None
|
||||||
db = inspect(self).session
|
db = inspect(self).session
|
||||||
token = new_token()
|
token = new_token()
|
||||||
orm_token = cls(user_id=self.id)
|
orm_token = APIToken(user_id=self.id)
|
||||||
orm_token.token = token
|
orm_token.token = token
|
||||||
db.add(orm_token)
|
db.add(orm_token)
|
||||||
db.commit()
|
db.commit()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def new_api_token(self):
|
|
||||||
"""Return a new API token"""
|
|
||||||
return self._new_token(APIToken)
|
|
||||||
|
|
||||||
def new_cookie_token(self):
|
|
||||||
"""Return a new cookie token"""
|
|
||||||
return self._new_token(CookieToken)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find(cls, db, name):
|
def find(cls, db, name):
|
||||||
"""Find a user by name.
|
"""Find a user by name.
|
||||||
@@ -345,8 +339,14 @@ class User(Base):
|
|||||||
inspect(self).session.commit()
|
inspect(self).session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Token(object):
|
class APIToken(Base):
|
||||||
"""Mixin for token tables, since we have two"""
|
"""An API token"""
|
||||||
|
__tablename__ = 'api_tokens'
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user_id(cls):
|
||||||
|
return Column(Integer, ForeignKey('users.id'))
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
hashed = Column(Unicode)
|
hashed = Column(Unicode)
|
||||||
prefix = Column(Unicode)
|
prefix = Column(Unicode)
|
||||||
@@ -367,10 +367,6 @@ class Token(object):
|
|||||||
self.hashed = hash_token(token, salt=self.salt_bytes, algorithm=self.algorithm)
|
self.hashed = hash_token(token, salt=self.salt_bytes, algorithm=self.algorithm)
|
||||||
self._token = token
|
self._token = token
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user_id(cls):
|
|
||||||
return Column(Integer, ForeignKey('users.id'))
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{cls}('{pre}...', user='{u}')>".format(
|
return "<{cls}('{pre}...', user='{u}')>".format(
|
||||||
cls=self.__class__.__name__,
|
cls=self.__class__.__name__,
|
||||||
@@ -389,18 +385,12 @@ class Token(object):
|
|||||||
# so we aren't comparing with all tokens
|
# so we aren't comparing with all tokens
|
||||||
prefix_match = db.query(cls).filter(cls.prefix==prefix)
|
prefix_match = db.query(cls).filter(cls.prefix==prefix)
|
||||||
for orm_token in prefix_match:
|
for orm_token in prefix_match:
|
||||||
if compare_token(orm_token.hashed, token):
|
if orm_token.match(token):
|
||||||
return orm_token
|
return orm_token
|
||||||
|
|
||||||
|
def match(self, token):
|
||||||
class APIToken(Token, Base):
|
"""Is this my token?"""
|
||||||
"""An API token"""
|
return compare_token(self.hashed, token)
|
||||||
__tablename__ = 'api_tokens'
|
|
||||||
|
|
||||||
|
|
||||||
class CookieToken(Token, Base):
|
|
||||||
"""A cookie token"""
|
|
||||||
__tablename__ = 'cookie_tokens'
|
|
||||||
|
|
||||||
|
|
||||||
def new_session(url="sqlite:///:memory:", reset=False, **kwargs):
|
def new_session(url="sqlite:///:memory:", reset=False, **kwargs):
|
||||||
|
@@ -44,7 +44,6 @@ def test_auth_api(app):
|
|||||||
# make a new cookie token
|
# make a new cookie token
|
||||||
user = db.query(orm.User).first()
|
user = db.query(orm.User).first()
|
||||||
api_token = user.new_api_token()
|
api_token = user.new_api_token()
|
||||||
cookie_token = user.new_cookie_token()
|
|
||||||
|
|
||||||
# check success:
|
# check success:
|
||||||
r = api_request(app, 'authorizations/token', api_token)
|
r = api_request(app, 'authorizations/token', api_token)
|
||||||
@@ -59,7 +58,7 @@ def test_auth_api(app):
|
|||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
r = api_request(app, 'authorizations/token', api_token,
|
r = api_request(app, 'authorizations/token', api_token,
|
||||||
headers={'Authorization': 'token: %s' % cookie_token},
|
headers={'Authorization': 'token: %s' % user.cookie_id},
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
@@ -80,14 +80,11 @@ def test_tokens(db):
|
|||||||
user = orm.User(name=u'inara')
|
user = orm.User(name=u'inara')
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
token = user.new_cookie_token()
|
token = user.new_api_token()
|
||||||
assert any(t.hashed == token for t in user.cookie_tokens)
|
assert any(t.match(token) for t in user.api_tokens)
|
||||||
user.new_cookie_token()
|
|
||||||
user.new_cookie_token()
|
|
||||||
user.new_api_token()
|
user.new_api_token()
|
||||||
assert len(user.api_tokens) == 1
|
assert len(user.api_tokens) == 2
|
||||||
assert len(user.cookie_tokens) == 3
|
found = orm.APIToken.find(db, token=token)
|
||||||
found = orm.CookieToken.find(db, token=token)
|
assert found.match(token)
|
||||||
assert found.hashed == token
|
|
||||||
found = orm.APIToken.find(db, 'something else')
|
found = orm.APIToken.find(db, 'something else')
|
||||||
assert found is None
|
assert found is None
|
||||||
|
Reference in New Issue
Block a user