This partially reverts commit c57aefd48b10ca3cabc9df162bc32efa62a6a21e. The code to check against hashed tokens remains, because existing tokens should still work. The unhashed tokens are necessary for fetching badges from Blender ID.
117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
import base64
|
|
import datetime
|
|
import hashlib
|
|
import logging
|
|
import typing
|
|
|
|
import bcrypt
|
|
|
|
from flask import abort, Blueprint, current_app, jsonify, request
|
|
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
|
|
from pillar.api.utils import utcnow
|
|
|
|
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 = current_app.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']
|
|
|
|
|
|
def get_local_user(username, 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)
|
|
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)
|
|
|
|
token = generate_and_store_token(user['_id'])
|
|
return jsonify(token=token['token'])
|
|
|
|
|
|
def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
|
|
"""Generates token based on random bits.
|
|
|
|
NOTE: the returned document includes the plain-text token.
|
|
DO NOT STORE OR LOG THIS unless there is a good reason to.
|
|
|
|
:param user_id: ObjectId of the owning user.
|
|
:param days: token will expire in this many days.
|
|
:param prefix: the token will be prefixed by these bytes, for easy identification.
|
|
:return: the token document with the token in plain text as well as hashed.
|
|
"""
|
|
|
|
if not isinstance(prefix, bytes):
|
|
raise TypeError('prefix must be bytes, not %s' % type(prefix))
|
|
|
|
import secrets
|
|
|
|
random_bits = secrets.token_bytes(32)
|
|
|
|
# Use 'xy' as altargs to prevent + and / characters from appearing.
|
|
# We never have to b64decode the string anyway.
|
|
token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
|
|
|
|
token_expiry = utcnow() + datetime.timedelta(days=days)
|
|
return store_token(user_id, token.decode('ascii'), token_expiry)
|
|
|
|
|
|
def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:
|
|
password = password.encode()
|
|
|
|
if isinstance(salt, str):
|
|
salt = salt.encode('utf-8')
|
|
|
|
hash = hashlib.sha256(password).digest()
|
|
encoded_password = base64.b64encode(hash)
|
|
hashed_password = bcrypt.hashpw(encoded_password, salt)
|
|
return hashed_password.decode('ascii')
|
|
|
|
|
|
def setup_app(app, url_prefix):
|
|
app.register_api_blueprint(blueprint, url_prefix=url_prefix)
|