diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 49fc7a4d..018dd24f 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -26,15 +26,11 @@ class CookieAPIHandler(APIHandler): @token_authenticated def get(self, cookie_name): cookie_value = self.request.body - btoken = self.get_secure_cookie(cookie_name, cookie_value) - if not btoken: - raise web.HTTPError(404) - token = btoken.decode('utf8', 'replace') - orm_token = orm.CookieToken.find(self.db, token) - if orm_token is None: + user = self._user_for_cookie(cookie_name, cookie_value) + if user is None: raise web.HTTPError(404) self.write(json.dumps({ - 'user' : orm_token.user.name, + 'user' : user.name, })) default_handlers = [ diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 0cf35b76..b2bf41ef 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -80,21 +80,26 @@ class BaseHandler(RequestHandler): user = orm_token.user user.last_activity = datetime.utcnow() 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): """get_current_user from a cookie token""" - btoken = self.get_secure_cookie(self.hub.server.cookie_name) - if btoken: - token = btoken.decode('utf8', 'replace') - cookie_token = orm.CookieToken.find(self.db, token) - if cookie_token: - return cookie_token.user - else: - # don't log the token itself - 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) - + user = self._user_for_cookie(self.hub.server.cookie_name) + if user: + return user + else: + # don't log the token itself + 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): """get current username""" user = self.get_current_user_token() @@ -128,19 +133,17 @@ class BaseHandler(RequestHandler): """Set login cookies for the Hub and single-user server.""" # create and set a new cookie token for the single-user server if user.server: - cookie_token = user.new_cookie_token() self.set_secure_cookie( user.server.cookie_name, - cookie_token, + user.cookie_id, path=user.server.base_url, ) # create and set a new cookie token for the hub if not self.get_current_user_cookie(): - cookie_token = user.new_cookie_token() self.set_secure_cookie( self.hub.server.cookie_name, - cookie_token, + user.cookie_id, path=self.hub.server.base_url) @gen.coroutine diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 300666db..7853a896 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -227,9 +227,11 @@ class User(Base): and multiple tokens used for authorization. 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, used for restoring state of a Spawner. @@ -244,7 +246,7 @@ class User(Base): last_activity = Column(DateTime, default=datetime.utcnow) api_tokens = relationship("APIToken", backref="user") - cookie_tokens = relationship("CookieToken", backref="user") + cookie_id = Column(Unicode, default=new_token) state = Column(JSONDict) spawner = None @@ -262,25 +264,17 @@ class User(Base): name=self.name, ) - def _new_token(self, cls): - """Create a new API or Cookie token""" + def new_api_token(self): + """Create a new API token""" assert self.id is not None db = inspect(self).session token = new_token() - orm_token = cls(user_id=self.id) + orm_token = APIToken(user_id=self.id) orm_token.token = token db.add(orm_token) db.commit() 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 def find(cls, db, name): """Find a user by name. @@ -345,8 +339,14 @@ class User(Base): inspect(self).session.commit() -class Token(object): - """Mixin for token tables, since we have two""" +class APIToken(Base): + """An API token""" + __tablename__ = 'api_tokens' + + @declared_attr + def user_id(cls): + return Column(Integer, ForeignKey('users.id')) + id = Column(Integer, primary_key=True) hashed = 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._token = token - @declared_attr - def user_id(cls): - return Column(Integer, ForeignKey('users.id')) - def __repr__(self): return "<{cls}('{pre}...', user='{u}')>".format( cls=self.__class__.__name__, @@ -389,18 +385,12 @@ class Token(object): # so we aren't comparing with all tokens prefix_match = db.query(cls).filter(cls.prefix==prefix) for orm_token in prefix_match: - if compare_token(orm_token.hashed, token): + if orm_token.match(token): return orm_token - - -class APIToken(Token, Base): - """An API token""" - __tablename__ = 'api_tokens' - - -class CookieToken(Token, Base): - """A cookie token""" - __tablename__ = 'cookie_tokens' + + def match(self, token): + """Is this my token?""" + return compare_token(self.hashed, token) def new_session(url="sqlite:///:memory:", reset=False, **kwargs): diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 451f029b..870b2437 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -44,7 +44,6 @@ def test_auth_api(app): # make a new cookie token user = db.query(orm.User).first() api_token = user.new_api_token() - cookie_token = user.new_cookie_token() # check success: r = api_request(app, 'authorizations/token', api_token) @@ -59,7 +58,7 @@ def test_auth_api(app): assert r.status_code == 403 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 diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 79ded386..738406aa 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -80,14 +80,11 @@ def test_tokens(db): user = orm.User(name=u'inara') db.add(user) db.commit() - token = user.new_cookie_token() - assert any(t.hashed == token for t in user.cookie_tokens) - user.new_cookie_token() - user.new_cookie_token() + token = user.new_api_token() + assert any(t.match(token) for t in user.api_tokens) user.new_api_token() - assert len(user.api_tokens) == 1 - assert len(user.cookie_tokens) == 3 - found = orm.CookieToken.find(db, token=token) - assert found.hashed == token + assert len(user.api_tokens) == 2 + found = orm.APIToken.find(db, token=token) + assert found.match(token) found = orm.APIToken.find(db, 'something else') assert found is None