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:
MinRK
2014-10-27 12:15:40 -07:00
parent 73706632d5
commit bce2be7401
6 changed files with 57 additions and 43 deletions

View File

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

View File

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

View File

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

View File

@@ -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() return {'Authorization': 'token %s' % token}
db.add(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

View File

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

View File

@@ -5,5 +5,6 @@ simplepam
sqlalchemy sqlalchemy
sqlalchemy-utils sqlalchemy-utils
cryptography cryptography
passlib
requests requests
six six