2016-04-26 12:33:48 +02:00
|
|
|
import base64
|
|
|
|
import hashlib
|
|
|
|
import logging
|
2017-03-03 14:14:36 +01:00
|
|
|
import typing
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-04-26 12:33:48 +02:00
|
|
|
import bcrypt
|
2016-08-19 09:19:06 +02:00
|
|
|
import datetime
|
2016-04-26 12:33:48 +02:00
|
|
|
from bson import tz_util
|
|
|
|
from flask import abort, Blueprint, current_app, jsonify, request
|
2016-08-19 09:19:06 +02:00
|
|
|
from pillar.api.utils.authentication import create_new_user_document
|
|
|
|
from pillar.api.utils.authentication import make_unique_username
|
|
|
|
from pillar.api.utils.authentication import store_token
|
2016-04-26 12:33:48 +02:00
|
|
|
|
|
|
|
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
|
2016-08-19 09:19:06 +02:00
|
|
|
r, _, _, status = current_app.post_internal('users', db_user)
|
2016-04-26 12:33:48 +02:00
|
|
|
if status != 201:
|
|
|
|
log.error('internal response: %r %r', status, r)
|
|
|
|
return abort(500)
|
|
|
|
# Return user ID
|
|
|
|
return r['_id']
|
|
|
|
|
|
|
|
|
2017-07-14 21:41:40 +02:00
|
|
|
def get_local_user(username, password):
|
2016-04-26 12:33:48 +02:00
|
|
|
# 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)
|
2017-07-14 21:41:40 +02:00
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
@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']
|
|
|
|
|
|
|
|
user = get_local_user(username, password)
|
2016-06-01 14:18:00 +02:00
|
|
|
|
|
|
|
token = generate_and_store_token(user['_id'])
|
|
|
|
return jsonify(token=token['token'])
|
|
|
|
|
|
|
|
|
2017-03-03 14:14:36 +01:00
|
|
|
def generate_and_store_token(user_id, days=15, prefix=b''):
|
2016-06-01 14:18:00 +02:00
|
|
|
"""Generates token based on random bits.
|
|
|
|
|
|
|
|
:param user_id: ObjectId of the owning user.
|
|
|
|
:param days: token will expire in this many days.
|
2017-03-03 14:14:36 +01:00
|
|
|
:param prefix: the token will be prefixed by these bytes, for easy identification.
|
2016-06-01 14:18:00 +02:00
|
|
|
:return: the token document.
|
|
|
|
"""
|
|
|
|
|
2017-03-03 14:16:29 +01:00
|
|
|
if not isinstance(prefix, bytes):
|
|
|
|
raise TypeError('prefix must be bytes, not %s' % type(prefix))
|
|
|
|
|
|
|
|
import secrets
|
|
|
|
|
|
|
|
random_bits = secrets.token_bytes(32)
|
2016-06-01 14:18:00 +02:00
|
|
|
|
|
|
|
# Use 'xy' as altargs to prevent + and / characters from appearing.
|
|
|
|
# We never have to b64decode the string anyway.
|
2017-03-03 14:14:36 +01:00
|
|
|
token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
|
2016-06-01 14:18:00 +02:00
|
|
|
|
|
|
|
token_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(days=days)
|
2017-03-03 14:14:36 +01:00
|
|
|
return store_token(user_id, token.decode('ascii'), token_expiry)
|
2016-04-26 12:33:48 +02:00
|
|
|
|
|
|
|
|
2017-03-03 14:14:36 +01:00
|
|
|
def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:
|
|
|
|
password = password.encode()
|
|
|
|
|
2017-03-03 12:00:24 +01:00
|
|
|
if isinstance(salt, str):
|
2016-04-26 12:33:48 +02:00
|
|
|
salt = salt.encode('utf-8')
|
2017-03-03 14:14:36 +01:00
|
|
|
|
|
|
|
hash = hashlib.sha256(password).digest()
|
|
|
|
encoded_password = base64.b64encode(hash)
|
|
|
|
hashed_password = bcrypt.hashpw(encoded_password, salt)
|
|
|
|
return hashed_password.decode('ascii')
|
2016-04-26 12:33:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
def setup_app(app, url_prefix):
|
2016-08-19 09:19:06 +02:00
|
|
|
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|