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:
Min RK
2014-10-28 14:59:25 -07:00
parent aed3efc557
commit ae7b92c55e
5 changed files with 50 additions and 65 deletions

View File

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

View File

@@ -80,21 +80,26 @@ class BaseHandler(RequestHandler):
user = orm_token.user user = orm_token.user
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"""
user = self.get_current_user_token() user = self.get_current_user_token()
@@ -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

View File

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

View File

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

View File

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