Added local accounts
This commit is contained in:
@@ -387,7 +387,9 @@ file_storage.setup_app(app, url_prefix='/storage')
|
|||||||
from modules.encoding import encoding
|
from modules.encoding import encoding
|
||||||
from modules.blender_id import blender_id
|
from modules.blender_id import blender_id
|
||||||
from modules import projects
|
from modules import projects
|
||||||
|
from modules import local_auth
|
||||||
|
|
||||||
app.register_blueprint(encoding, url_prefix='/encoding')
|
app.register_blueprint(encoding, url_prefix='/encoding')
|
||||||
app.register_blueprint(blender_id, url_prefix='/blender_id')
|
app.register_blueprint(blender_id, url_prefix='/blender_id')
|
||||||
projects.setup_app(app, url_prefix='/p')
|
projects.setup_app(app, url_prefix='/p')
|
||||||
|
local_auth.setup_app(app, url_prefix='/auth')
|
||||||
|
84
pillar/application/modules/local_auth.py
Normal file
84
pillar/application/modules/local_auth.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import rsa
|
||||||
|
import bcrypt
|
||||||
|
from bson import tz_util
|
||||||
|
from eve.methods.post import post_internal
|
||||||
|
|
||||||
|
from flask import abort, Blueprint, current_app, jsonify, request
|
||||||
|
|
||||||
|
from application.utils.authentication import store_token
|
||||||
|
from application.utils.authentication import create_new_user_document
|
||||||
|
from application.utils.authentication import make_unique_username
|
||||||
|
|
||||||
|
blueprint = Blueprint('authentication', __name__)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_credentials(user, provider):
|
||||||
|
return next((credentials for credentials in user['auth'] if 'provider'
|
||||||
|
in credentials and credentials['provider'] == provider), None)
|
||||||
|
|
||||||
|
|
||||||
|
def create_local_user(email, password):
|
||||||
|
"""For internal user only. Given username and password, create a user."""
|
||||||
|
# Hash the password
|
||||||
|
hashed_password = hash_password(password, bcrypt.gensalt())
|
||||||
|
db_user = create_new_user_document(email, '', email, provider='local',
|
||||||
|
token=hashed_password)
|
||||||
|
# Make username unique
|
||||||
|
db_user['username'] = make_unique_username(email)
|
||||||
|
# Create the user
|
||||||
|
r, _, _, status = post_internal('users', db_user)
|
||||||
|
if status != 201:
|
||||||
|
log.error('internal response: %r %r', status, r)
|
||||||
|
return abort(500)
|
||||||
|
# Return user ID
|
||||||
|
return r['_id']
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/make-token', methods=['POST'])
|
||||||
|
def make_token():
|
||||||
|
"""Direct login for a user, without OAuth, using local database. Generates
|
||||||
|
a token that is passed back to Pillar Web and used in subsequent
|
||||||
|
transactions.
|
||||||
|
|
||||||
|
:return: a token string
|
||||||
|
"""
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
|
||||||
|
# Look up user in db
|
||||||
|
users_collection = current_app.data.driver.db['users']
|
||||||
|
user = users_collection.find_one({'username': username})
|
||||||
|
if not user:
|
||||||
|
return abort(403)
|
||||||
|
# Check if user has "local" auth type
|
||||||
|
credentials = get_auth_credentials(user, 'local')
|
||||||
|
if not credentials:
|
||||||
|
return abort(403)
|
||||||
|
# Verify password
|
||||||
|
salt = credentials['token']
|
||||||
|
hashed_password = hash_password(password, salt)
|
||||||
|
if hashed_password != credentials['token']:
|
||||||
|
return abort(403)
|
||||||
|
# Generate Token
|
||||||
|
token = base64.b64encode(rsa.randnum.read_random_bits(256))
|
||||||
|
# TODO look into alternative implementations
|
||||||
|
token_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(
|
||||||
|
days=15)
|
||||||
|
store_token(user['_id'], token, token_expiry)
|
||||||
|
return jsonify(token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password, salt):
|
||||||
|
if isinstance(salt, unicode):
|
||||||
|
salt = salt.encode('utf-8')
|
||||||
|
encoded_password = base64.b64encode(hashlib.sha256(password).digest())
|
||||||
|
return bcrypt.hashpw(encoded_password, salt)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_app(app, url_prefix):
|
||||||
|
app.register_blueprint(blueprint, url_prefix=url_prefix)
|
@@ -83,7 +83,7 @@ def find_token(token, is_subclient_token=False, **extra_filters):
|
|||||||
return db_token
|
return db_token
|
||||||
|
|
||||||
|
|
||||||
def store_token(user_id, token, token_expiry, oauth_subclient_id):
|
def store_token(user_id, token, token_expiry, oauth_subclient_id=False):
|
||||||
"""Stores an authentication token.
|
"""Stores an authentication token.
|
||||||
|
|
||||||
:returns: the token document from MongoDB
|
:returns: the token document from MongoDB
|
||||||
@@ -92,9 +92,11 @@ def store_token(user_id, token, token_expiry, oauth_subclient_id):
|
|||||||
token_data = {
|
token_data = {
|
||||||
'user': user_id,
|
'user': user_id,
|
||||||
'token': token,
|
'token': token,
|
||||||
'is_subclient_token': bool(oauth_subclient_id),
|
|
||||||
'expire_time': token_expiry,
|
'expire_time': token_expiry,
|
||||||
}
|
}
|
||||||
|
if oauth_subclient_id:
|
||||||
|
token_data['is_subclient_token'] = True
|
||||||
|
|
||||||
r, _, _, status = post_internal('tokens', token_data)
|
r, _, _, status = post_internal('tokens', token_data)
|
||||||
|
|
||||||
if status not in {200, 201}:
|
if status not in {200, 201}:
|
||||||
@@ -120,17 +122,20 @@ def create_new_user(email, username, user_id):
|
|||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
def create_new_user_document(email, user_id, username):
|
def create_new_user_document(email, user_id, username, provider='blender-id',
|
||||||
"""Creates a new user document, without storing it in MongoDB."""
|
token=''):
|
||||||
|
"""Creates a new user document, without storing it in MongoDB. The token
|
||||||
|
parameter is a password in case provider is "local".
|
||||||
|
"""
|
||||||
|
|
||||||
user_data = {
|
user_data = {
|
||||||
'full_name': username,
|
'full_name': username,
|
||||||
'username': username,
|
'username': username,
|
||||||
'email': email,
|
'email': email,
|
||||||
'auth': [{
|
'auth': [{
|
||||||
'provider': 'blender-id',
|
'provider': provider,
|
||||||
'user_id': str(user_id),
|
'user_id': str(user_id),
|
||||||
'token': ''}], # TODO: remove 'token' field altogether.
|
'token': token}],
|
||||||
'settings': {
|
'settings': {
|
||||||
'email_communications': 1
|
'email_communications': 1
|
||||||
},
|
},
|
||||||
|
@@ -774,5 +774,10 @@ def expire_all_project_links(project_uuid):
|
|||||||
print('Expired %i links' % result.matched_count)
|
print('Expired %i links' % result.matched_count)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def register_local_user(email, password):
|
||||||
|
from application.modules.local_auth import create_local_user
|
||||||
|
create_local_user(email, password)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
manager.run()
|
manager.run()
|
||||||
|
@@ -85,11 +85,13 @@ users_schema = {
|
|||||||
'schema': {
|
'schema': {
|
||||||
'provider': {
|
'provider': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'allowed': ["blender-id",],
|
'allowed': ["blender-id", "local"],
|
||||||
},
|
},
|
||||||
'user_id' : {
|
'user_id': {
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
},
|
},
|
||||||
|
# A token is considered a "password" in case the provider is
|
||||||
|
# "local".
|
||||||
'token': {
|
'token': {
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
}
|
}
|
||||||
@@ -360,7 +362,6 @@ tokens_schema = {
|
|||||||
'is_subclient_token': {
|
'is_subclient_token': {
|
||||||
'type': 'boolean',
|
'type': 'boolean',
|
||||||
'required': False,
|
'required': False,
|
||||||
'default': False,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
tests/test_local_auth.py
Normal file
76
tests/test_local_auth.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from bson import tz_util
|
||||||
|
|
||||||
|
from common_test_class import AbstractPillarTest
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAuthTest(AbstractPillarTest):
|
||||||
|
def create_test_user(self):
|
||||||
|
from application.modules import local_auth
|
||||||
|
with self.app.test_request_context():
|
||||||
|
user_id = local_auth.create_local_user('koro@example.com', 'oti')
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
def test_create_local_user(self):
|
||||||
|
user_id = self.create_test_user()
|
||||||
|
|
||||||
|
with self.app.test_request_context():
|
||||||
|
users = self.app.data.driver.db['users']
|
||||||
|
db_user = users.find_one(user_id)
|
||||||
|
self.assertIsNotNone(db_user)
|
||||||
|
|
||||||
|
def test_login_existing_user(self):
|
||||||
|
user_id = self.create_test_user()
|
||||||
|
|
||||||
|
resp = self.client.post('/auth/make-token',
|
||||||
|
data={'username': 'koro',
|
||||||
|
'password': 'oti'})
|
||||||
|
self.assertEqual(200, resp.status_code, resp.data)
|
||||||
|
|
||||||
|
token_info = json.loads(resp.data)
|
||||||
|
token = token_info['token']
|
||||||
|
|
||||||
|
headers = {'Authorization': self.make_header(token)}
|
||||||
|
resp = self.client.get('/users/%s' % user_id,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(200, resp.status_code, resp.data)
|
||||||
|
|
||||||
|
def test_login_expired_token(self):
|
||||||
|
user_id = self.create_test_user()
|
||||||
|
|
||||||
|
resp = self.client.post('/auth/make-token',
|
||||||
|
data={'username': 'koro',
|
||||||
|
'password': 'oti'})
|
||||||
|
self.assertEqual(200, resp.status_code, resp.data)
|
||||||
|
|
||||||
|
token_info = json.loads(resp.data)
|
||||||
|
token = token_info['token']
|
||||||
|
|
||||||
|
with self.app.test_request_context():
|
||||||
|
tokens = self.app.data.driver.db['tokens']
|
||||||
|
|
||||||
|
exp = datetime.datetime.now(tz=tz_util.utc) - datetime.timedelta(1)
|
||||||
|
result = tokens.update_one({'token': token},
|
||||||
|
{'$set': {'expire_time': exp}})
|
||||||
|
self.assertEqual(1, result.modified_count)
|
||||||
|
|
||||||
|
headers = {'Authorization': self.make_header(token)}
|
||||||
|
resp = self.client.get('/users/%s' % user_id,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(403, resp.status_code, resp.data)
|
||||||
|
|
||||||
|
def test_login_nonexistant_user(self):
|
||||||
|
resp = self.client.post('/auth/make-token',
|
||||||
|
data={'username': 'proog',
|
||||||
|
'password': 'oti'})
|
||||||
|
|
||||||
|
self.assertEqual(403, resp.status_code, resp.data)
|
||||||
|
|
||||||
|
def test_login_bad_pwd(self):
|
||||||
|
resp = self.client.post('/auth/make-token',
|
||||||
|
data={'username': 'koro',
|
||||||
|
'password': 'koro'})
|
||||||
|
|
||||||
|
self.assertEqual(403, resp.status_code, resp.data)
|
Reference in New Issue
Block a user