mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 12:03:00 +00:00
only store hashed tokens
- use PasswordType - store first 4 bytes for filtering by prefix since we can't filter by equality on the hashed value. - user.new_foo_token() returns token string, not ORM object
This commit is contained in:
@@ -15,7 +15,7 @@ from .base import APIHandler
|
|||||||
class TokenAPIHandler(APIHandler):
|
class TokenAPIHandler(APIHandler):
|
||||||
@token_authenticated
|
@token_authenticated
|
||||||
def get(self, token):
|
def get(self, token):
|
||||||
orm_token = self.db.query(orm.APIToken).filter(orm.APIToken.token == token).first()
|
orm_token = orm.APIToken.find(self.db, token)
|
||||||
if orm_token is None:
|
if orm_token is None:
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
self.write(json.dumps({
|
self.write(json.dumps({
|
||||||
@@ -30,7 +30,7 @@ class CookieAPIHandler(APIHandler):
|
|||||||
if not btoken:
|
if not btoken:
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
token = btoken.decode('utf8', 'replace')
|
token = btoken.decode('utf8', 'replace')
|
||||||
orm_token = self.db.query(orm.CookieToken).filter(orm.CookieToken.token == token).first()
|
orm_token = orm.CookieToken.find(self.db, token)
|
||||||
if orm_token is None:
|
if orm_token is None:
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
self.write(json.dumps({
|
self.write(json.dumps({
|
||||||
|
@@ -129,22 +129,18 @@ class BaseHandler(RequestHandler):
|
|||||||
# 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()
|
cookie_token = user.new_cookie_token()
|
||||||
self.db.add(cookie_token)
|
|
||||||
self.db.commit()
|
|
||||||
self.set_secure_cookie(
|
self.set_secure_cookie(
|
||||||
user.server.cookie_name,
|
user.server.cookie_name,
|
||||||
cookie_token.token,
|
cookie_token,
|
||||||
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()
|
cookie_token = user.new_cookie_token()
|
||||||
self.db.add(cookie_token)
|
|
||||||
self.db.commit()
|
|
||||||
self.set_secure_cookie(
|
self.set_secure_cookie(
|
||||||
self.hub.server.cookie_name,
|
self.hub.server.cookie_name,
|
||||||
cookie_token.token,
|
cookie_token,
|
||||||
path=self.hub.server.base_url)
|
path=self.hub.server.base_url)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
from binascii import b2a_hex
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import errno
|
import errno
|
||||||
import json
|
import json
|
||||||
@@ -19,14 +18,14 @@ from tornado.httpclient import HTTPRequest, AsyncHTTPClient, HTTPError
|
|||||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
inspect,
|
inspect,
|
||||||
Column, Integer, String, ForeignKey, Unicode, Binary, Boolean,
|
Column, Integer, ForeignKey, Unicode, Binary, Boolean,
|
||||||
DateTime,
|
DateTime,
|
||||||
)
|
)
|
||||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy_utils.types import EncryptedType
|
from sqlalchemy_utils.types import EncryptedType, PasswordType
|
||||||
|
|
||||||
from .utils import random_port, url_path_join, wait_for_server, wait_for_http_server
|
from .utils import random_port, url_path_join, wait_for_server, wait_for_http_server
|
||||||
|
|
||||||
@@ -38,6 +37,7 @@ def new_token(*args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
return text_type(uuid.uuid4().hex)
|
return text_type(uuid.uuid4().hex)
|
||||||
|
|
||||||
|
PASSWORD_SCHEMES = ['pbkdf2_sha512']
|
||||||
|
|
||||||
class JSONDict(TypeDecorator):
|
class JSONDict(TypeDecorator):
|
||||||
"""Represents an immutable structure as a json-encoded string.
|
"""Represents an immutable structure as a json-encoded string.
|
||||||
@@ -273,8 +273,15 @@ class User(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _new_token(self, cls):
|
def _new_token(self, cls):
|
||||||
|
"""Create a new API or Cookie token"""
|
||||||
assert self.id is not None
|
assert self.id is not None
|
||||||
return cls(token=new_token(), user_id=self.id)
|
db = inspect(self).session
|
||||||
|
token = new_token()
|
||||||
|
orm_token = cls(user_id=self.id)
|
||||||
|
orm_token.token = token
|
||||||
|
db.add(orm_token)
|
||||||
|
db.commit()
|
||||||
|
return token
|
||||||
|
|
||||||
def new_api_token(self):
|
def new_api_token(self):
|
||||||
"""Return a new API token"""
|
"""Return a new API token"""
|
||||||
@@ -306,7 +313,6 @@ class User(Base):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
api_token = self.new_api_token()
|
api_token = self.new_api_token()
|
||||||
db.add(api_token)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -317,7 +323,7 @@ class User(Base):
|
|||||||
)
|
)
|
||||||
# we are starting a new server, make sure it doesn't restore state
|
# we are starting a new server, make sure it doesn't restore state
|
||||||
spawner.clear_state()
|
spawner.clear_state()
|
||||||
spawner.api_token = api_token.token
|
spawner.api_token = api_token
|
||||||
|
|
||||||
yield spawner.start()
|
yield spawner.start()
|
||||||
spawner.start_polling()
|
spawner.start_polling()
|
||||||
@@ -351,15 +357,32 @@ class User(Base):
|
|||||||
|
|
||||||
class Token(object):
|
class Token(object):
|
||||||
"""Mixin for token tables, since we have two"""
|
"""Mixin for token tables, since we have two"""
|
||||||
token = Column(EncryptedType(Unicode, key=b''), primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
hashed = Column(PasswordType(schemes=PASSWORD_SCHEMES))
|
||||||
|
prefix = Column(Unicode)
|
||||||
|
prefix_length = 4
|
||||||
|
_token = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self):
|
||||||
|
"""plaintext tokens will only be accessible for tokens created during this session"""
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
@token.setter
|
||||||
|
def token(self, token):
|
||||||
|
"""Store the hashed value and prefix for a token"""
|
||||||
|
self.prefix = token[:self.prefix_length]
|
||||||
|
self.hashed = token
|
||||||
|
self._token = token
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def user_id(cls):
|
def user_id(cls):
|
||||||
return Column(Integer, ForeignKey('users.id'))
|
return Column(Integer, ForeignKey('users.id'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{cls}('{t}', user='{u}')>".format(
|
return "<{cls}('{pre}...', user='{u}')>".format(
|
||||||
cls=self.__class__.__name__,
|
cls=self.__class__.__name__,
|
||||||
t=self.token,
|
pre=self.prefix,
|
||||||
u=self.user.name,
|
u=self.user.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -369,7 +392,13 @@ class Token(object):
|
|||||||
|
|
||||||
Returns None if not found.
|
Returns None if not found.
|
||||||
"""
|
"""
|
||||||
return db.query(cls).filter(cls.token==token).first()
|
prefix = token[:cls.prefix_length]
|
||||||
|
# since we can't filter on hashed values, filter on prefix
|
||||||
|
# so we aren't comparing with all tokens
|
||||||
|
prefix_match = db.query(cls).filter(cls.prefix==prefix)
|
||||||
|
for orm_token in prefix_match:
|
||||||
|
if orm_token.hashed == token:
|
||||||
|
return orm_token
|
||||||
|
|
||||||
|
|
||||||
class APIToken(Token, Base):
|
class APIToken(Token, Base):
|
||||||
|
@@ -20,13 +20,8 @@ def auth_header(db, name):
|
|||||||
user = find_user(db, name)
|
user = find_user(db, name)
|
||||||
if user is None:
|
if user is None:
|
||||||
user = add_user(db, name=name)
|
user = add_user(db, name=name)
|
||||||
if not user.api_tokens:
|
|
||||||
token = user.new_api_token()
|
token = user.new_api_token()
|
||||||
db.add(token)
|
return {'Authorization': 'token %s' % token}
|
||||||
db.commit()
|
|
||||||
else:
|
|
||||||
token = user.api_tokens[0]
|
|
||||||
return {'Authorization': 'token %s' % token.token}
|
|
||||||
|
|
||||||
def api_request(app, *api_path, **kwargs):
|
def api_request(app, *api_path, **kwargs):
|
||||||
"""Make an API request"""
|
"""Make an API request"""
|
||||||
@@ -49,25 +44,22 @@ 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()
|
||||||
db.add(api_token)
|
|
||||||
cookie_token = user.new_cookie_token()
|
cookie_token = user.new_cookie_token()
|
||||||
db.add(cookie_token)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# check success:
|
# check success:
|
||||||
r = api_request(app, 'authorizations/token', api_token.token)
|
r = api_request(app, 'authorizations/token', api_token)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
reply = r.json()
|
reply = r.json()
|
||||||
assert reply['user'] == user.name
|
assert reply['user'] == user.name
|
||||||
|
|
||||||
# check fail
|
# check fail
|
||||||
r = api_request(app, 'authorizations/token', api_token.token,
|
r = api_request(app, 'authorizations/token', api_token,
|
||||||
headers={'Authorization': 'no sir'},
|
headers={'Authorization': 'no sir'},
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
r = api_request(app, 'authorizations/token', api_token.token,
|
r = api_request(app, 'authorizations/token', api_token,
|
||||||
headers={'Authorization': 'token: %s' % cookie_token.token},
|
headers={'Authorization': 'token: %s' % cookie_token},
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
@@ -81,17 +81,13 @@ def test_tokens(db):
|
|||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
token = user.new_cookie_token()
|
token = user.new_cookie_token()
|
||||||
db.add(token)
|
assert any(t.hashed == token for t in user.cookie_tokens)
|
||||||
db.commit()
|
user.new_cookie_token()
|
||||||
assert token in user.cookie_tokens
|
user.new_cookie_token()
|
||||||
db.add(user.new_cookie_token())
|
user.new_api_token()
|
||||||
db.add(user.new_cookie_token())
|
|
||||||
db.add(user.new_api_token())
|
|
||||||
db.commit()
|
|
||||||
assert len(user.api_tokens) == 1
|
assert len(user.api_tokens) == 1
|
||||||
assert len(user.cookie_tokens) == 3
|
assert len(user.cookie_tokens) == 3
|
||||||
|
found = orm.CookieToken.find(db, token=token)
|
||||||
found = orm.CookieToken.find(db, token=token.token)
|
assert found.hashed == token
|
||||||
assert found.token == token.token
|
found = orm.APIToken.find(db, 'something else')
|
||||||
found = orm.APIToken.find(db, token.token)
|
|
||||||
assert found is None
|
assert found is None
|
||||||
|
@@ -5,5 +5,6 @@ simplepam
|
|||||||
sqlalchemy
|
sqlalchemy
|
||||||
sqlalchemy-utils
|
sqlalchemy-utils
|
||||||
cryptography
|
cryptography
|
||||||
|
passlib
|
||||||
requests
|
requests
|
||||||
six
|
six
|
||||||
|
Reference in New Issue
Block a user