def test_23_verify_empty_hash(self): "test verify() allows hash=None" handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt] cc = CryptContext(handlers, policy=None) self.assertTrue(not cc.verify("test", None)) for handler in handlers: self.assertTrue(not cc.verify("test", None, scheme=handler.name))
def change_password(conn, user_id, new_pass): """ Changes the specified user_id password :param conn: PostgreSQL connection object :param user_id: Users id :param new_pass: New password that will be set to the user :return: True if the password was changed, None otherwise """ logger.info('Changing password for user_id %s', user_id) default_crypt_context = CryptContext( ['pbkdf2_sha512', 'md5_crypt'], deprecated=['md5_crypt'], ) conn['cursor'].execute(("select password, password_crypt" " from res_users where id = %(id)s"), {'id': user_id}) res_user = conn['cursor'].fetchone() res = None if res_user: if not res_user[0] and res_user[1]: logger.info('Encrypted password') crypted_passwd = default_crypt_context.encrypt(new_pass) conn['cursor'].execute(("update res_users set password=''," " password_crypt=%(passwd)s where id = %(id)s"), {'passwd': crypted_passwd, 'id': user_id}) res = True elif res_user[0] and res_user[1] == '': logger.info('Non encrypted password') conn['cursor'].execute("update res_users set password=%(passwd)s where id = %(id)s", {'passwd': new_pass, 'id': user_id}) res = True return res
def validate_passwd(self, senha): #Criando um objeto que usará criptografia do método shs256, rounds default de 80000 cripto = CryptContext(schemes="sha256_crypt") #Comparando o valor da string com o valor criptografado okornot = cripto.verify(senha, self.senha) return okornot
def login(): ''' Let users log in. ''' if request.method == 'GET': return render_template('index.html', next=request.args.get('next')) else: username = request.form['wyr_username'] password = request.form['wyr_password'] remember = request.form.getlist('remember') next = request.form['next'] #first see if username exists if User.query.filter_by(username=username).count() == 1: #get their encrypted pass and check it user = User.query.filter_by(username=username).first() myctx = CryptContext(schemes=['pbkdf2_sha256']) if myctx.verify(password, user.password) == True: if remember: login_user(user, remember=True) else: login_user(user) flash('Welcome back, {}.'.format(username)) else: flash('Sorry, the password is incorrect.') else: flash('Username does not exist.') if next: return redirect('https://www.whatyouveread.com' + next) return redirect(url_for('main.index'))
class HashHandler: default_scheme = 'sha512_crypt' valid_schemes = ('sha512_crypt', 'pbkdf2_sha512') def __init__(self): try: from passlib.context import CryptContext self.enabled = True except ImportError: self.enabled = False if self.enabled: self.context = CryptContext(schemes=self.valid_schemes) def encrypt(self, *args, **kwargs): return self.context.encrypt(*args, **kwargs) def verify(self, password, hash): is_valid = False if self.enabled: is_valid = self.context.verify(password, hash) del password return is_valid
def register_context(): bad_config = "" constructed_context = None try: if context: if isinstance(context, CryptContext): constructed_context = context else: msg = "'context' must be an instance of passlib.CryptContext" bad_config = msg elif ini_string: constructed_context = CryptContext.from_string(ini_string) elif ini_file: constructed_context = CryptContext.from_path(ini_file) elif context_dict: constructed_context = CryptContext(**context_dict) else: # no required arguments have been passed, error bad_config = 'requires a CryptContext or configuration data' except IOError: bad_config = "unable to open %s" % ini_file except ValueError: bad_config = "received invalid or incompatible configuration options" except KeyError: bad_config = "received unknown or forbidden configuration options" except TypeError: bad_config = "received configuration options of the wrong type" if bad_config: raise ConfigurationError("set_password_context %s" % bad_config) config.registry.password_context = constructed_context
def activate(): ''' Activate user account - finish the sign up process now that the email is verified - get user's password, do checks on it, and insert user into database ''' #send user to form to set password if hash is good if request.method == 'GET': #first, pull user's email and username out of hash hash = request.args.get('code') serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) try: decoded = serializer.loads(hash, salt='sign_up', max_age=3600) except SignatureExpired: flash('Activation period expired. Please sign up again.') return redirect(url_for('main.index')) except: flash("Error activating your account. Please sign up again below.") return redirect(url_for('main.index')) return render_template('activate.html', username=decoded[0], email=decoded[1]) # get user's desired password, check, add account if request.method == 'POST': username = request.form['username'] email = request.form['email'] password = request.form['password'] confirm_password = request.form['confirm_password'] #checks - password if password != confirm_password: flash("Your passwords did not match. Please try again.") return render_template('activate.html', username=username, email=email) if len(password) < 5: flash("Your password is too short. Please try again.") return render_template('activate.html', username=username, email=email) #checks - if user already completed sign up, redirect if User.query.filter_by(username=username).count() > 0: flash("You've already activated your account.") return redirect(url_for('main.index')) # use passlib to encrypt password myctx = CryptContext(schemes=['pbkdf2_sha256']) hashed_password = myctx.encrypt(password) # create a salt alphabet = string.ascii_letters + string.digits salt = ''.join(secrets.choice(alphabet) for i in range(32)) #add user user = User(username, hashed_password, salt, email) db.session.add(user) db.session.commit() login_user(user) flash('Thank you. Your account has been activated.') return redirect(url_for('main.settings'))
def change_password(): ''' Let users change password ''' if request.method == 'GET': return render_template('change_password.html') elif request.method == 'POST': if request.form['submit'] == 'Cancel': flash('Password change cancelled.') return redirect(url_for('main.settings')) current_password = request.form['wyr_current_password'] new_password = request.form['wyr_new_password'] confirm_password = request.form['wyr_confirm_password'] #first verify current password myctx = CryptContext(schemes=['pbkdf2_sha256']) if myctx.verify(current_password, current_user.password) == True: #password checks if len(new_password) < 5: flash('Password is too short. Please try again.') return redirect(url_for('main.change_password')) elif new_password != confirm_password: flash('The confirmation password did not match the new password you entered.') return redirect(url_for('main.change_password')) else: #use passlib to encrypt password myctx = CryptContext(schemes=['pbkdf2_sha256']) hash = myctx.encrypt(new_password) current_user.password = hash db.session.commit() # send user email to confirm, allow reset of password #hash for confirm change serializer = URLSafeSerializer(current_app.config['SECRET_KEY']) email_hash = serializer.dumps([current_user.email], salt='reset_password') to = current_user.email subject = 'Password Change' text = """The password for your What You've Read account has been changed. If this was not you, someone has access to your account. You should <a href="http://www.whatyouveread.com/reset_password?code={}">reset your password</a> immediately.<br> <br> -Kris @ What You've Read""".format(email_hash) common.send_simple_message(to, subject, text) flash('Your password has been updated.') return redirect(url_for('main.settings')) else: flash('Password is incorrect.') return redirect(url_for('main.change_password')) else: return abort(405)
def generate_admin_password_hash(admin_password, salt): # Flask password hash generation approach for Cloudify 4.x where x<=2 pwd_hmac = base64.b64encode( # Encodes put in to keep hmac happy with unicode strings hmac.new(salt.encode('utf-8'), admin_password.encode('utf-8'), hashlib.sha512).digest() ).decode('ascii') # This ctx is nothing to do with a cloudify ctx. pass_ctx = CryptContext(schemes=['pbkdf2_sha256']) return pass_ctx.encrypt(pwd_hmac)
def test_21_identify(self): "test identify() border cases" handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] cc = CryptContext(handlers, policy=None) #check unknown hash self.assertEqual(cc.identify('$9$232323123$1287319827'), None) self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True) #make sure "None" is accepted self.assertEqual(cc.identify(None), None) self.assertRaises(ValueError, cc.identify, None, required=True)
def walter_credentials(request, walter, clear_walter_cached_credentials, session): password = "******" cc = CryptContext(["bcrypt_sha256"]) credentials = cc.encrypt(password) thirty_from_now = datetime.datetime.now() + datetime.timedelta(days=30) credential = CredentialModel(user_id=walter.pk_id, credential=credentials, expiration_dt=thirty_from_now) session = session() session.add(credential) session.commit() return credentials
def _verify_by_hashcode(pincode, hashcode): logger.debug('Will test against %s' % hashcode) from passlib.context import CryptContext myctx = CryptContext(schemes=['sha256_crypt', 'sha512_crypt', 'bcrypt', 'md5_crypt']) try: if not myctx.verify(pincode, hashcode): raise totpcgi.UserPincodeError('Pincode did not match.') return True except ValueError: raise totpcgi.UserPincodeError('Unsupported hashcode format')
def test_25_verify_and_update(self): "test verify_and_update()" cc = CryptContext(**self.sample_policy_1) #create some hashes h1 = cc.encrypt("password", scheme="des_crypt") h2 = cc.encrypt("password", scheme="sha256_crypt") #check bad password, deprecated hash ok, new_hash = cc.verify_and_update("wrongpass", h1) self.assertFalse(ok) self.assertIs(new_hash, None) #check bad password, good hash ok, new_hash = cc.verify_and_update("wrongpass", h2) self.assertFalse(ok) self.assertIs(new_hash, None) #check right password, deprecated hash ok, new_hash = cc.verify_and_update("password", h1) self.assertTrue(ok) self.assertTrue(cc.identify(new_hash), "sha256_crypt") #check right password, good hash ok, new_hash = cc.verify_and_update("password", h2) self.assertTrue(ok) self.assertIs(new_hash, None)
def test_01_replace(self): "test replace()" cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) self.assertIs(cc.policy.get_handler(), hash.md5_crypt) cc2 = cc.replace() self.assertIsNot(cc2, cc) self.assertIs(cc2.policy, cc.policy) cc3 = cc.replace(default="bsdi_crypt") self.assertIsNot(cc3, cc) self.assertIsNot(cc3.policy, cc.policy) self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt)
def f1(): if len(sys.argv) != 3: print "ERROR args" return 0 myctx = CryptContext(schemes=["pbkdf2_sha256", "pbkdf2_sha512"]) pw = base64.b64decode(sys.argv[1]) hash = base64.b64decode(sys.argv[2]) if not myctx.identify(hash): return 2 return 1 if myctx.verify(pw, hash) else 0
def test_95_context_algs(self): """test handling of 'algs' in context object""" handler = self.handler from passlib.context import CryptContext c1 = CryptContext(["scram"], scram__algs="sha1,md5") h = c1.hash("dummy") self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"]) self.assertFalse(c1.needs_update(h)) c2 = c1.copy(scram__algs="sha1") self.assertFalse(c2.needs_update(h)) c2 = c1.copy(scram__algs="sha1,sha256") self.assertTrue(c2.needs_update(h))
def __init__(self, **kwargs): """ All keyword arguments are forwarded to the construction of a `passlib.context.CryptContext` object. The following usage will create a password column that will automatically hash new passwords as `pbkdf2_sha512` but still compare passwords against pre-existing `md5_crypt` hashes. As passwords are compared; the password hash in the database will be updated to be `pbkdf2_sha512`. :: class Model(Base): password = sa.Column(PasswordType( schemes=[ 'pbkdf2_sha512', 'md5_crypt' ], deprecated=['md5_crypt'] )) """ # Bail if passlib is not found. if passlib is None: raise ImproperlyConfigured( "'passlib' is required to use 'PasswordType'") # Construct the passlib crypt context. self.context = CryptContext(**kwargs)
def initialize(self): self.name = 'user' self.ensureIndices(['login', 'email', 'groupInvites.groupId', 'size', 'created']) self.prefixSearchFields = ( 'login', ('firstName', 'i'), ('lastName', 'i')) self.ensureTextIndex({ 'login': 1, 'firstName': 1, 'lastName': 1 }, language='none') self.exposeFields(level=AccessType.READ, fields=( '_id', 'login', 'public', 'firstName', 'lastName', 'admin', 'created')) self.exposeFields(level=AccessType.ADMIN, fields=( 'size', 'email', 'groups', 'groupInvites', 'status', 'emailVerified')) # To ensure compatibility with authenticator apps, other defaults shouldn't be changed self._TotpFactory = TOTP.using( # An application secret could be set here, if it existed wallet=None ) self._cryptContext = CryptContext( schemes=['bcrypt'] ) events.bind('model.user.save.created', CoreEventHandler.USER_SELF_ACCESS, self._grantSelfAccess) events.bind('model.user.save.created', CoreEventHandler.USER_DEFAULT_FOLDERS, self._addDefaultFolders)
def __init__(self, key, data=None, digest=None, cryptcontext=None): if cryptcontext is None: # @todo make the CryptContext schemes not hard-coded self.cryptcontext = CryptContext(schemes=['ldap_pbkdf2_sha512'], default='ldap_pbkdf2_sha512', all__vary_rounds=0.1) else: self.cryptcontext = cryptcontext if data is not None and digest is not None: self.data = data # okay, first verify the digest we got to check for a mismatch -- # we have to use the passlib verify, in case an older scheme was # used, instead of just calculating the new digest and comparing # strings if not self.cryptcontext.verify(data, digest): raise SaltboxException("Given data doesn't verify with given digest") # if self.digest_str isn't our default hash scheme, recalculate it # and save it as default hash scheme # @todo this always recalculates; wasteful self.digest() elif data is not None: self.data = data self.digest() elif digest is not None: self.digest_str = digest
def generate_password_hashes(self, password): """ Refer to passlib documentation for adding new hashers: https://pythonhosted.org/passlib/lib/passlib.hash.html """ password_schemes = ['pbkdf2_sha256', 'sha512_crypt', 'sha256_crypt', 'des_crypt', 'md5_crypt'] pwd_context = CryptContext(schemes=password_schemes) self.sha512_crypt = pwd_context.hash(password, 'sha512_crypt') self.sha256_crypt = pwd_context.hash(password, 'sha256_crypt') self.des_crypt = pwd_context.hash(password, 'des_crypt') self.md5_crypt = pwd_context.hash(password, 'md5_crypt') self.save()
def encrypt_pwd(toyz_settings, pwd): """ Use the passlib module to create a hash for the given password. Parameters - toyz_settings ( :py:class:`toyz.utils.core.ToyzSettings` ): Settings for the current application - pwd (*string* ): password the user has entered Returns - pwd_hash (*string* ): Password hash to be stored for the given password. If passwords are not encrypted, this will just return the ``pwd`` passed to the function. """ from passlib.context import CryptContext pwd_context = CryptContext(**toyz_settings.security.pwd_context) return pwd_context.encrypt(pwd)
def test_01_replace(self): "test replace()" cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) self.assertIs(cc.policy.get_handler(), hash.md5_crypt) cc2 = cc.replace() self.assertIsNot(cc2, cc) # NOTE: was not able to maintain backward compatibility with this... ##self.assertIs(cc2.policy, cc.policy) cc3 = cc.replace(default="bsdi_crypt") self.assertIsNot(cc3, cc) # NOTE: was not able to maintain backward compatibility with this... ##self.assertIs(cc3.policy, cc.policy) self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt)
def __init__(self, config): """Create a password context for hashing and verification. :param config: The `IConfiguration` instance. """ config_string = load_external(config.passwords.configuration) self._context = CryptContext.from_string(config_string)
def generate_password_hashes(self, password): """ Generate password hashes with SHA512, PBKDF2/SHA-256 and DES crypt. Refer to passlib documentation for adding new hashers: https://pythonhosted.org/passlib/lib/passlib.hash.html """ password_schemes = ['pbkdf2_sha256', 'sha512_crypt', 'des_crypt'] pwd_context = CryptContext(schemes=password_schemes) self.pbkdf2_sha256 = pwd_context.encrypt(password, scheme='pbkdf2_sha256') self.sha512_crypt = pwd_context.encrypt(password, scheme='sha512_crypt') self.des_crypt = pwd_context.encrypt(password, scheme='des_crypt') self.save()
def test_22_verify(self): "test verify() scheme kwd" handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"] cc = CryptContext(handlers, policy=None) h = hash.md5_crypt.encrypt("test") #check base verify self.assertTrue(cc.verify("test", h)) self.assertTrue(not cc.verify("notest", h)) #check verify using right alg self.assertTrue(cc.verify('test', h, scheme='md5_crypt')) self.assertTrue(not cc.verify('notest', h, scheme='md5_crypt')) #check verify using wrong alg self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt')
def test_02_no_handlers(self): "test no handlers" #check constructor... cc = CryptContext() self.assertRaises(KeyError, cc.identify, 'hash', required=True) self.assertRaises(KeyError, cc.encrypt, 'secret') self.assertRaises(KeyError, cc.verify, 'secret', 'hash') #check updating policy after the fact... cc = CryptContext(['md5_crypt']) p = CryptPolicy(schemes=[]) cc.policy = p self.assertRaises(KeyError, cc.identify, 'hash', required=True) self.assertRaises(KeyError, cc.encrypt, 'secret') self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
class User(BaseState): """ :type name: str :type hashed_password: str :type role: list :type gateway_docker_id: str :type gateway_urls: list :type network_id: str """ api_in_attrs = ['name', 'role'] api_out_attrs = ['name', 'role', 'gateway_urls'] private_attrs = ['hashed_password', 'gateway_docker_id', 'network_id'] def __init__(self, state): super().__init__(state) self.name = '' self.hashed_password = '' self.role = '' self.gateway_docker_id = None self.gateway_urls = [] self.network_id = None # Links to other objects self.applications = [] self.pwd_context = CryptContext(schemes=["sha512_crypt"], sha512_crypt__default_rounds=get_conf().passlib_rounds) def set_password(self, pw): self.hashed_password = self.pwd_context.encrypt(pw) def verify_password(self, pw): return self.pwd_context.verify(pw, self.hashed_password) @property def owner(self): return self def can_see_non_owner_objects(self): return self.role == 'admin' def set_gateway_urls(self, cont_info): socks_url = 'socks://' + cont_info['ports']['1080/tcp'][0] + ':' + cont_info['ports']['1080/tcp'][1] self.gateway_urls = [socks_url]
def test_00_constructor(self): "test constructor" #create crypt context using handlers cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt]) c,b,a = cc.policy.iter_handlers() self.assertIs(a, hash.des_crypt) self.assertIs(b, hash.bsdi_crypt) self.assertIs(c, hash.md5_crypt) #create context using names cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"]) c,b,a = cc.policy.iter_handlers() self.assertIs(a, hash.des_crypt) self.assertIs(b, hash.bsdi_crypt) self.assertIs(c, hash.md5_crypt) # policy kwd policy = cc.policy cc = CryptContext(policy=policy) self.assertEqual(cc.to_dict(), policy.to_dict()) cc = CryptContext(policy=policy, default="bsdi_crypt") self.assertNotEqual(cc.to_dict(), policy.to_dict()) self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"], default="bsdi_crypt")) self.assertRaises(TypeError, setattr, cc, 'policy', None) self.assertRaises(TypeError, CryptContext, policy='x')
def __init__(self): try: from passlib.context import CryptContext self.enabled = True except ImportError: self.enabled = False if self.enabled: self.context = CryptContext(schemes=self.valid_schemes)
def __init__(self, config): # Is the context coming from a file system or Python path? if config.passwords.path.startswith('python:'): resource_path = config.passwords.path[7:] package, dot, resource = resource_path.rpartition('.') config_string = resource_string(package, resource + '.cfg') else: with open(config.passwords.path, 'rb') as fp: config_string = fp.read() self._context = CryptContext.from_string(config_string)
@classmethod def from_string(cls, hash): salt, chk = uh.parse_mc2(hash, u'') if chk is None: raise ValueError('missing checksum') return cls(salt=salt, checksum=chk) def to_string(self): return to_hash_str(u'%s$%s' % (self.salt, self.checksum or u'')) def _calc_checksum(self, secret): return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8') _CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'], deprecated=['cubicwebmd5crypt', 'des_crypt']) verify_and_update = _CRYPTO_CTX.verify_and_update def crypt_password(passwd, salt=None): """return the encrypted password using the given salt or a generated one """ if salt is None: return _CRYPTO_CTX.hash(passwd).encode('ascii') # empty hash, accept any password for backwards compat if salt == '': return salt try: if _CRYPTO_CTX.verify(passwd, salt): return salt
from datetime import datetime, timedelta import jwt from fastapi import Depends, HTTPException from jwt import PyJWTError from passlib.context import CryptContext from pydantic import EmailStr from starlette import status from config import apisecrets, oauth2_scheme from models import User from v1.services.user import UserService user_service = UserService() PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto") SECRET_KEY = apisecrets.SECRET_KEY ACCESS_TOKEN_ALGORITHM = 'HS256' CREDENTIALS_EXCEPTION = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials.", headers={"WWW-Authenticate": "Bearer"}) class AuthService: def valid_password(self, plain_pass: str, hashed_pass: str) -> bool: return PWD_CONTEXT.verify(plain_pass, hashed_pass) def authenticate_user(self, email: EmailStr, password: str) -> User: user = user_service.get_user_by_email(email) if not user: return if not self.valid_password(password, user.password):
def context(self): """ per-test CryptContext() created from .config. """ return CryptContext._norm_source(self.config)
from itertools import chain from flask import Flask, render_template, flash, redirect, url_for, session, request, logging, json from flask_mysqldb import MySQL from flask_mail import Mail, Message from wtforms import Form, StringField, TextAreaField, PasswordField, validators, SelectField, HiddenField from wtforms.fields.html5 import DateTimeLocalField from passlib.context import CryptContext from functools import wraps from wtforms import form from config import config import os cryptcontext = CryptContext(schemes=["sha256_crypt", "md5_crypt", "des_crypt"]) # Schemes for encrypting passwords app = Flask(__name__) # Config MySQL app.config['MYSQL_HOST'] = config.MYSQL_HOST app.config['MYSQL_USER'] = config.MYSQL_USER app.config['MYSQL_PASSWORD'] = config.MYSQL_PASSWORD app.config['MYSQL_DB'] = config.MYSQL_DB app.config['MYSQL_CURSORCLASS'] = config.MYSQL_CURSORCLASS app.secret_key = os.urandom(12).hex() # init MySQL mysql = MySQL(app) # init Mail # Ensure 2 Factor Authentication is off and allow less secure app access mail = Mail(app)
HASHING_PREFIX_DJANGO = "pbkdf2_sha256" # The prefix of the hashed using pbkdf2_sha256 algorithm in Passlib HASHING_PREFIX_PBKDF2_SHA256 = "$pbkdf2-sha256" # This will never be a valid encoded hash UNUSABLE_PASSWORD_PREFIX = '!' # Number of random chars to add after UNUSABLE_PASSWORD_PREFIX UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 HASHING_KEY = "HashingKey" pwd_context = CryptContext( # The list of hashes that we support schemes=["pbkdf2_sha256", "des_crypt"], # The default hashing mechanism default="pbkdf2_sha256", # We set the number of rounds that should be used... pbkdf2_sha256__default_rounds=8000, ) def create_unusable_pass(): return UNUSABLE_PASSWORD_PREFIX + get_random_string( UNUSABLE_PASSWORD_SUFFIX_LENGTH) def is_password_usable(enc_pass): if enc_pass is None or enc_pass.startswith(UNUSABLE_PASSWORD_PREFIX): return False
from passlib.context import CryptContext from jwt import decode as jwt_decode from db import Account password_crypto_context = CryptContext( schemes=["pbkdf2_sha256"], default="pbkdf2_sha256", pbkdf2_sha256__default_rounds=30000, ) def password_hash(cleartext: str): return password_crypto_context.hash(cleartext) def password_compare(cleartext: str, hashed: str): return password_crypto_context.verify(cleartext, hashed) def read_yggt(access_token_string: str): """ :param access_token_string: 32 or 36 char UUID or JWT :return: yggt value """ if len(access_token_string) == 32: return access_token_string elif len(access_token_string) == 36: return access_token_string.replace("-", "") else: return jwt_decode(jwt=access_token_string, verify=False)["yggt"]
# -*- coding: utf-8 -*- from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password, hashed_password): """ Is the hash of the two values equal :param plain_password: plaintext :param hashed_password: hash password :return: """ return pwd_context.verify(plain_password, hashed_password) def gen_password_hash(data): """ Generate password hash :param data: plaintext data :return: """ return pwd_context.hash(data)
#!/usr/bin/python3 import argparse import json from os import _exit try: from passlib.context import CryptContext except ImportError: print("You need to run") print("python -m pip install passlib bcrypt") _exit(0) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") try: with open("../web/web_user_db.json", "r") as f: users_db = json.loads(f.read()) except FileNotFoundError: users_db = {} print("web_user_db.json file does not exist, will create.") users_db = dict() except json.decoder.JSONDecodeError: print("empty web_user_db.json") # Create an empty dictionary to add to users_db = dict() parser = argparse.ArgumentParser( description="Create user for web authentication") parser.add_argument("--user", help="Specifiy the username", required=True)
class Praetorian: """ Comprises the implementation for the flask-praetorian flask extension. Provides a tool that allows password authentication and token provision for applications and designated endpoints """ def __init__(self, app=None, user_class=None, is_blacklisted=None): self.pwd_ctx = None self.hash_scheme = None self.salt = None if app is not None and user_class is not None: self.init_app(app, user_class, is_blacklisted) def init_app(self, app, user_class, is_blacklisted=None): """ Initializes the Praetorian extension :param: app: The flask app to bind this extension to :param: user_class: The class used to interact with user data :param: is_blacklisted: A method that may optionally be used to check the token against a blacklist when access or refresh is requested Should take the jti for the token to check as a single argument. Returns True if the jti is blacklisted, False otherwise. By default, always returns False. """ PraetorianError.require_condition( app.config.get('SECRET_KEY') is not None, "There must be a SECRET_KEY app config setting set", ) self.hash_autoupdate = app.config.get( 'PRAETORIAN_HASH_AUTOUPDATE', DEFAULT_HASH_AUTOUPDATE, ) self.hash_autotest = app.config.get( 'PRAETORIAN_HASH_AUTOTEST', DEFAULT_HASH_AUTOTEST, ) self.pwd_ctx = CryptContext( schemes=app.config.get('PRAETORIAN_HASH_ALLOWED_SCHEMES', DEFAULT_HASH_ALLOWED_SCHEMES), default=app.config.get('PRAETORIAN_HASH_SCHEME', DEFAULT_HASH_SCHEME), deprecated=app.config.get('PRAETORIAN_HASH_DEPRECATED_SCHEMES', DEFAULT_HASH_DEPRECATED_SCHEMES), ) valid_schemes = self.pwd_ctx.schemes() PraetorianError.require_condition( self.hash_scheme in valid_schemes or self.hash_scheme is None, "If {} is set, it must be one of the following schemes: {}", 'PRAETORIAN_HASH_SCHEME', valid_schemes, ) self.user_class = self._validate_user_class(user_class) self.is_blacklisted = is_blacklisted or (lambda t: False) self.encode_key = app.config['SECRET_KEY'] self.allowed_algorithms = app.config.get( 'JWT_ALLOWED_ALGORITHMS', DEFAULT_JWT_ALLOWED_ALGORITHMS, ) self.encode_algorithm = app.config.get( 'JWT_ALGORITHM', DEFAULT_JWT_ALGORITHM, ) self.access_lifespan = app.config.get( 'JWT_ACCESS_LIFESPAN', DEFAULT_JWT_ACCESS_LIFESPAN, ) self.refresh_lifespan = app.config.get( 'JWT_REFRESH_LIFESPAN', DEFAULT_JWT_REFRESH_LIFESPAN, ) self.reset_lifespan = app.config.get( 'JWT_RESET_LIFESPAN', DEFAULT_JWT_RESET_LIFESPAN, ) self.header_name = app.config.get( 'JWT_HEADER_NAME', DEFAULT_JWT_HEADER_NAME, ) self.header_type = app.config.get( 'JWT_HEADER_TYPE', DEFAULT_JWT_HEADER_TYPE, ) self.user_class_validation_method = app.config.get( 'USER_CLASS_VALIDATION_METHOD', DEFAULT_USER_CLASS_VALIDATION_METHOD, ) self.confirmation_template = app.config.get( 'PRAETORIAN_CONFIRMATION_TEMPLATE', DEFAULT_CONFIRMATION_TEMPLATE, ) self.confirmation_uri = app.config.get('PRAETORIAN_CONFIRMATION_URI', ) self.confirmation_sender = app.config.get( 'PRAETORIAN_CONFIRMATION_SENDER', ) self.confirmation_subject = app.config.get( 'PRAETORIAN_CONFIRMATION_SUBJECT', DEFAULT_CONFIRMATION_SUBJECT, ) self.reset_template = app.config.get( 'PRAETORIAN_RESET_TEMPLATE', DEFAULT_RESET_TEMPLATE, ) self.reset_uri = app.config.get('PRAETORIAN_RESET_URI', ) self.reset_sender = app.config.get('PRAETORIAN_RESET_SENDER', ) self.reset_subject = app.config.get( 'PRAETORIAN_RESET_SUBJECT', DEFAULT_RESET_SUBJECT, ) if isinstance(self.access_lifespan, dict): self.access_lifespan = pendulum.duration(**self.access_lifespan) elif isinstance(self.access_lifespan, str): self.access_lifespan = duration_from_string(self.access_lifespan) ConfigurationError.require_condition( isinstance(self.access_lifespan, datetime.timedelta), "access lifespan was not configured", ) if isinstance(self.refresh_lifespan, dict): self.refresh_lifespan = pendulum.duration(**self.refresh_lifespan) if isinstance(self.refresh_lifespan, str): self.refresh_lifespan = duration_from_string(self.refresh_lifespan) ConfigurationError.require_condition( isinstance(self.refresh_lifespan, datetime.timedelta), "refresh lifespan was not configured", ) if not app.config.get('DISABLE_PRAETORIAN_ERROR_HANDLER'): app.register_error_handler( PraetorianError, PraetorianError.build_error_handler(), ) self.is_testing = app.config.get('TESTING', False) if not hasattr(app, 'extensions'): app.extensions = {} app.extensions['praetorian'] = self return app @classmethod def _validate_user_class(cls, user_class): """ Validates the supplied user_class to make sure that it has the class methods necessary to function correctly. Requirements: - ``lookup`` method. Accepts a string parameter, returns instance - ``identify`` method. Accepts an identity parameter, returns instance """ PraetorianError.require_condition( getattr(user_class, 'lookup', None) is not None, textwrap.dedent(""" The user_class must have a lookup class method: user_class.lookup(<str>) -> <user instance> """), ) PraetorianError.require_condition( getattr(user_class, 'identify', None) is not None, textwrap.dedent(""" The user_class must have an identify class method: user_class.identify(<identity>) -> <user instance> """), ) # TODO: Figure out how to check for an identity property return user_class def authenticate(self, username, password): """ Verifies that a password matches the stored password for that username. If verification passes, the matching user instance is returned """ PraetorianError.require_condition( self.user_class is not None, "Praetorian must be initialized before this method is available", ) user = self.user_class.lookup(username) MissingUserError.require_condition( user is not None, 'Could not find the requested user', ) AuthenticationError.require_condition( self._verify_password(password, user.password), 'The password is incorrect', ) """ If we are set to PRAETORIAN_HASH_AUTOUPDATE then check our hash and if needed, update the user. The developer is responsible for using the returned user object and updating the data storage endpoint. Else, if we are set to PRAETORIAN_HASH_AUTOTEST then check out hash and return exception if our hash is using the wrong scheme, but don't modify the user. """ if self.hash_autoupdate: self.verify_and_update(user=user, password=password) elif self.hash_autotest: self.verify_and_update(user=user) return user def _verify_password(self, raw_password, hashed_password): """ Verifies that a plaintext password matches the hashed version of that password using the stored passlib password context """ PraetorianError.require_condition( self.pwd_ctx is not None, "Praetorian must be initialized before this method is available", ) return self.pwd_ctx.verify(raw_password, hashed_password) @deprecated('Use `hash_password` instead.') def encrypt_password(self, raw_password): """ *NOTE* This should be deprecated as its an incorrect definition for what is actually being done -- we are hashing, not encrypting """ return self.hash_password(raw_password) def error_handler(self, error): """ Provides a flask error handler that is used for PraetorianErrors (and derived exceptions). """ warnings.warn( """ error_handler is deprecated. Use FlaskBuzz.build_error_handler instead """, warnings.DeprecationWarning, ) return error.jsonify(), error.status_code, error.headers def _check_user(self, user): """ Checks to make sure that a user is valid. First, checks that the user is not None. If this check fails, a MissingUserError is raised. Next, checks if the user has a validation method. If the method does not exist, the check passes. If the method exists, it is called. If the result of the call is not truthy, an InvalidUserError is raised """ MissingUserError.require_condition( user is not None, 'Could not find the requested user', ) user_validate_method = getattr(user, self.user_class_validation_method, None) if user_validate_method is None: return InvalidUserError.require_condition( user_validate_method(), "The user is not valid or has had access revoked", ) def encode_jwt_token(self, user, override_access_lifespan=None, override_refresh_lifespan=None, bypass_user_check=False, is_registration_token=False, is_reset_token=False, **custom_claims): """ Encodes user data into a jwt token that can be used for authorization at protected endpoints :param: override_access_lifespan: Override's the instance's access lifespan to set a custom duration after which the new token's accessability will expire. May not exceed the refresh_lifespan :param: override_refresh_lifespan: Override's the instance's refresh lifespan to set a custom duration after which the new token's refreshability will expire. :param: bypass_user_check: Override checking the user for being real/active. Used for registration token generation. :param: is_registration_token: Indicates that the token will be used only for email-based registration :param: custom_claims: Additional claims that should be packed in the payload. Note that any claims supplied here must be JSON compatible types """ ClaimCollisionError.require_condition( set(custom_claims.keys()).isdisjoint(RESERVED_CLAIMS), "The custom claims collide with required claims", ) if not bypass_user_check: self._check_user(user) moment = pendulum.now('UTC') if override_refresh_lifespan is None: refresh_lifespan = self.refresh_lifespan else: refresh_lifespan = override_refresh_lifespan refresh_expiration = (moment + refresh_lifespan).int_timestamp if override_access_lifespan is None: access_lifespan = self.access_lifespan else: access_lifespan = override_access_lifespan access_expiration = min( (moment + access_lifespan).int_timestamp, refresh_expiration, ) payload_parts = { 'iat': moment.int_timestamp, 'exp': access_expiration, 'jti': str(uuid.uuid4()), 'id': user.identity, 'rls': ','.join(user.rolenames), REFRESH_EXPIRATION_CLAIM: refresh_expiration, } if is_registration_token: payload_parts[IS_REGISTRATION_TOKEN_CLAIM] = True if is_reset_token: payload_parts[IS_RESET_TOKEN_CLAIM] = True flask.current_app.logger.debug( "Attaching custom claims: {}".format(custom_claims), ) payload_parts.update(custom_claims) return jwt.encode( payload_parts, self.encode_key, self.encode_algorithm, ).decode('utf-8') def encode_eternal_jwt_token(self, user, **custom_claims): """ This utility function encodes a jwt token that never expires .. note:: This should be used sparingly since the token could become a security concern if it is ever lost. If you use this method, you should be sure that your application also implements a blacklist so that a given token can be blocked should it be lost or become a security concern """ return self.encode_jwt_token(user, override_access_lifespan=VITAM_AETERNUM, override_refresh_lifespan=VITAM_AETERNUM, **custom_claims) def refresh_jwt_token(self, token, override_access_lifespan=None): """ Creates a new token for a user if and only if the old token's access permission is expired but its refresh permission is not yet expired. The new token's refresh expiration moment is the same as the old token's, but the new token's access expiration is refreshed :param: token: The existing jwt token that needs to be replaced with a new, refreshed token :param: override_access_lifespan: Override's the instance's access lifespan to set a custom duration after which the new token's accessability will expire. May not exceed the refresh lifespan """ moment = pendulum.now('UTC') data = self.extract_jwt_token(token, access_type=AccessType.refresh) user = self.user_class.identify(data['id']) self._check_user(user) if override_access_lifespan is None: access_lifespan = self.access_lifespan else: access_lifespan = override_access_lifespan refresh_expiration = data[REFRESH_EXPIRATION_CLAIM] access_expiration = min( (moment + access_lifespan).int_timestamp, refresh_expiration, ) custom_claims = { k: v for (k, v) in data.items() if k not in RESERVED_CLAIMS } payload_parts = { 'iat': moment.int_timestamp, 'exp': access_expiration, 'jti': data['jti'], 'id': data['id'], 'rls': ','.join(user.rolenames), REFRESH_EXPIRATION_CLAIM: refresh_expiration, } payload_parts.update(custom_claims) return jwt.encode( payload_parts, self.encode_key, self.encode_algorithm, ).decode('utf-8') def extract_jwt_token(self, token, access_type=AccessType.access): """ Extracts a data dictionary from a jwt token """ # Note: we disable exp verification because we will do it ourselves with InvalidTokenHeader.handle_errors('failed to decode JWT token'): data = jwt.decode( token, self.encode_key, algorithms=self.allowed_algorithms, options={'verify_exp': False}, ) self._validate_jwt_data(data, access_type=access_type) return data def _validate_jwt_data(self, data, access_type): """ Validates that the data for a jwt token is valid """ MissingClaimError.require_condition( 'jti' in data, 'Token is missing jti claim', ) BlacklistedError.require_condition( not self.is_blacklisted(data['jti']), 'Token has a blacklisted jti', ) MissingClaimError.require_condition( 'id' in data, 'Token is missing id field', ) MissingClaimError.require_condition( 'exp' in data, 'Token is missing exp claim', ) MissingClaimError.require_condition( REFRESH_EXPIRATION_CLAIM in data, 'Token is missing {} claim'.format(REFRESH_EXPIRATION_CLAIM), ) moment = pendulum.now('UTC').int_timestamp if access_type == AccessType.access: MisusedRegistrationToken.require_condition( IS_REGISTRATION_TOKEN_CLAIM not in data, "registration token used for access") MisusedResetToken.require_condition( IS_RESET_TOKEN_CLAIM not in data, "password reset token used for access") ExpiredAccessError.require_condition( moment <= data['exp'], 'access permission has expired', ) elif access_type == AccessType.refresh: MisusedRegistrationToken.require_condition( IS_REGISTRATION_TOKEN_CLAIM not in data, "registration token used for refresh") MisusedResetToken.require_condition( IS_RESET_TOKEN_CLAIM not in data, "password reset token used for refresh") EarlyRefreshError.require_condition( moment > data['exp'], 'access permission for token has not expired. may not refresh', ) ExpiredRefreshError.require_condition( moment <= data[REFRESH_EXPIRATION_CLAIM], 'refresh permission for token has expired', ) elif access_type == AccessType.register: ExpiredAccessError.require_condition( moment <= data['exp'], 'register permission has expired', ) InvalidRegistrationToken.require_condition( IS_REGISTRATION_TOKEN_CLAIM in data, "invalid registration token used for verification") MisusedResetToken.require_condition( IS_RESET_TOKEN_CLAIM not in data, "password reset token used for registration") elif access_type == AccessType.reset: MisusedRegistrationToken.require_condition( IS_REGISTRATION_TOKEN_CLAIM not in data, "registration token used for reset") ExpiredAccessError.require_condition( moment <= data['exp'], 'reset permission has expired', ) InvalidResetToken.require_condition( IS_RESET_TOKEN_CLAIM in data, "invalid reset token used for verification") def _unpack_header(self, headers): """ Unpacks a jwt token from a request header """ jwt_header = headers.get(self.header_name) MissingTokenHeader.require_condition( jwt_header is not None, "JWT token not found in headers under '{}'", self.header_name, ) match = re.match(self.header_type + r'\s*([\w\.-]+)', jwt_header) InvalidTokenHeader.require_condition( match is not None, "JWT header structure is invalid", ) token = match.group(1) return token def read_token_from_header(self): """ Unpacks a jwt token from the current flask request """ return self._unpack_header(flask.request.headers) def pack_header_for_user(self, user, override_access_lifespan=None, override_refresh_lifespan=None, **custom_claims): """ Encodes a jwt token and packages it into a header dict for a given user :param: user: The user to package the header for :param: override_access_lifespan: Override's the instance's access lifespan to set a custom duration after which the new token's accessability will expire. May not exceed the refresh_lifespan :param: override_refresh_lifespan: Override's the instance's refresh lifespan to set a custom duration after which the new token's refreshability will expire. :param: custom_claims: Additional claims that should be packed in the payload. Note that any claims supplied here must be JSON compatible types """ token = self.encode_jwt_token( user, override_access_lifespan=override_access_lifespan, override_refresh_lifespan=override_refresh_lifespan, **custom_claims) return {self.header_name: self.header_type + ' ' + token} def send_registration_email(self, email, user=None, template=None, confirmation_sender=None, confirmation_uri=None, subject=None, override_access_lifespan=None): """ Sends a registration email to a new user, containing a time expiring token usable for validation. This requires your application is initiliazed with a `mail` extension, which supports Flask-Mail's `Message()` object and a `send()` method. Returns a dict containing the information sent, along with the `result` from mail send. :param: user: The user object to tie claim to (username, id, email, etc) :param: template: HTML Template for confirmation email. If not provided, a stock entry is used :param: confirmation_sender: The sender that shoudl be attached to the confirmation email. Overrides the PRAETORIAN_CONFIRMRATION_SENDER config setting :param: confirmation_uri: The uri that should be visited to complete email registration. Should usually be a uri to a frontend or external service that calls a 'finalize' method in the api to complete registration. Will override the PRAETORIAN_CONFIRMATION_URI config setting :param: subject: The registration email subject. Will override the PRAETORIAN_CONFIRMATION_SUBJECT config setting. :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN to set an access lifespan for the registration token. """ if subject is None: subject = self.confirmation_subject if confirmation_uri is None: confirmation_uri = self.confirmation_uri sender = confirmation_sender or self.confirmation_sender flask.current_app.logger.debug( "Generating token with lifespan: {}".format( override_access_lifespan)) custom_token = self.encode_jwt_token( user, override_access_lifespan=override_access_lifespan, bypass_user_check=True, is_registration_token=True, ) return self.send_token_email(email, user, template, confirmation_sender, confirmation_uri, subject, custom_token=custom_token, sender=sender) def send_reset_email(self, email, template=None, reset_sender=None, reset_uri=None, subject=None, override_access_lifespan=None): """ Sends a password reset email to a user, containing a time expiring token usable for validation. This requires your application is initiliazed with a `mail` extension, which supports Flask-Mail's `Message()` object and a `send()` method. Returns a dict containing the information sent, along with the `result` from mail send. :param: email: The email address to attempt to send to :param: template: HTML Template for reset email. If not provided, a stock entry is used :param: confirmation_sender: The sender that shoudl be attached to the reset email. Overrides the PRAETORIAN_RESET_SENDER config setting :param: confirmation_uri: The uri that should be visited to complete password reset. Should usually be a uri to a frontend or external service that calls the 'validate_reset_token()' method in the api to complete reset. Will override the PRAETORIAN_RESET_URI config setting :param: subject: The reset email subject. Will override the PRAETORIAN_RESET_SUBJECT config setting. :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN to set an access lifespan for the registration token. """ if subject is None: subject = self.reset_subject if reset_uri is None: reset_uri = self.reset_uri sender = reset_sender or self.reset_sender user = self.user_class.lookup(email) MissingUserError.require_condition( user is not None, 'Could not find the requested user', ) flask.current_app.logger.debug( "Generating token with lifespan: {}".format( override_access_lifespan)) custom_token = self.encode_jwt_token( user, override_access_lifespan=override_access_lifespan, bypass_user_check=False, is_reset_token=True, ) return self.send_token_email(user.email, user, template, reset_sender, reset_uri, subject, custom_token=custom_token, sender=sender) def send_token_email(self, email, user=None, template=None, action_sender=None, action_uri=None, subject=None, override_access_lifespan=None, custom_token=None, sender='no-reply@praetorian'): """ Sends an email to a user, containing a time expiring token usable for several actions. This requires your application is initiliazed with a `mail` extension, which supports Flask-Mail's `Message()` object and a `send()` method. Returns a dict containing the information sent, along with the `result` from mail send. :param: email: The email address to use (username, id, email, etc) :param: user: The user object to tie claim to (username, id, email, etc) :param: template: HTML Template for confirmation email. If not provided, a stock entry is used :param: action_sender: The sender that should be attached to the confirmation email. :param: action_uri: The uri that should be visited to complete the token action. :param: subject: The email subject. :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN to set an access lifespan for the registration token. """ notification = { 'result': None, 'message': None, 'user': str(user), 'email': email, 'token': custom_token, 'subject': subject, 'confirmation_uri': action_uri, # backwards compatibility 'action_uri': action_uri, } PraetorianError.require_condition( action_sender, "A sender is required to send confirmation email", ) PraetorianError.require_condition( custom_token, "A custom_token is required to send notification email", ) if template is None: with open(self.confirmation_template) as fh: template = fh.read() with PraetorianError.handle_errors('fail sending email'): flask.current_app.logger.debug( "NOTIFICATION: {}".format(notification)) jinja_tmpl = jinja2.Template(template) notification['message'] = jinja_tmpl.render(notification).strip() msg = Message(html=notification['message'], sender=action_sender, subject=notification['subject'], recipients=[notification['email']]) flask.current_app.logger.debug("Sending email to {}".format(email)) notification['result'] = flask.current_app.extensions['mail'].send( msg) return notification def get_user_from_registration_token(self, token): """ Gets a user based on the registration token that is supplied. Verifies that the token is a regisration token and that the user can be properly retrieved """ data = self.extract_jwt_token(token, access_type=AccessType.register) flask.current_app.logger.debug("DATA: {}".format(data)) user_id = data.get('id') PraetorianError.require_condition( user_id is not None, "Could not fetch an id from the registration token", ) user = self.user_class.identify(user_id) PraetorianError.require_condition( user is not None, "Could not identify the user from the registration token", ) return user def validate_reset_token(self, token): """ Validates a password reset request based on the reset token that is supplied. Verifies that the token is a reset token and that the user can be properly retrieved """ data = self.extract_jwt_token(token, access_type=AccessType.reset) user_id = data.get('id') PraetorianError.require_condition( user_id is not None, "Could not fetch an id from the reset token", ) user = self.user_class.identify(user_id) PraetorianError.require_condition( user is not None, "Could not identify the user from the reset token", ) return user def hash_password(self, raw_password): """ Hashes a plaintext password using the stored passlib password context """ PraetorianError.require_condition( self.pwd_ctx is not None, "Praetorian must be initialized before this method is available", ) """ `scheme` is now set with self.pwd_ctx.update(default=scheme) due to the depreciation in upcoming passlib 2.0. zillions of warnings suck. """ return self.pwd_ctx.hash(raw_password) def verify_and_update(self, user=None, password=None): """ Validate a password hash contained in the user object is hashed with the defined hash scheme (PRAETORIAN_HASH_SCHEME). If not, raise an Exception of `LegacySchema`, unless the `password` arguement is provided, in which case an attempt to call `user.save()` will be made, updating the hashed password to the currently desired hash scheme (PRAETORIAN_HASH_SCHEME). :param: user: The user object to tie claim to (username, id, email, etc). *MUST* include the hashed password field, defined as `user.password` :param: password: The user's provide password from login. If present, this is used to validate and then attempt to update with the new PRAETORIAN_HASH_SCHEME scheme. """ if self.pwd_ctx.needs_update(user.password): if password: (rv, updated) = self.pwd_ctx.verify_and_update( password, user.password, ) AuthenticationError.require_condition( rv, "Could not verify password", ) user.password = updated else: used_hash = self.pwd_ctx.identify(user.password) desired_hash = self.hash_scheme raise LegacyScheme( "Hash using non-current scheme '{}'. Use '{}' instead.". format(used_hash, desired_hash)) return user
import traceback from fastapi.security import OAuth2PasswordBearer from passlib.context import CryptContext from functools import wraps from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from fastapi_mako import FastAPIMako import databases import aiohttp from config import DB_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth') pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') mako = FastAPIMako() db_engine = create_engine( DB_URL ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) Base = declarative_base() # AioDataBase = databases.Database(DB_URL.replace('+pymysql', '')) class AioDataBase(): async def __aenter__(self): db = databases.Database(DB_URL.replace('+pymysql', '')) await db.connect() self.db = db
from fastapi import Body, APIRouter from fastapi.encoders import jsonable_encoder from fastapi.security import HTTPBasicCredentials from passlib.context import CryptContext from database.database import admin_collection from auth.jwt_handler import signJWT from database.database import add_admin from models.admin import AdminModel router = APIRouter() hash_helper = CryptContext(schemes=["bcrypt"]) @router.post("/login") async def admin_login(admin_credentials: HTTPBasicCredentials = Body(...)): # NEW CODE admin_user = await admin_collection.find_one( {"email": admin_credentials.username}, {"_id": 0}) if (admin_user): password = hash_helper.verify(admin_credentials.password, admin_user["password"]) if (password): return signJWT(admin_credentials.username) return "Incorrect email or password" return "Incorrect email or password"
class DatabaseUserService: def __init__(self, session, *, ratelimiters=None, metrics): if ratelimiters is None: ratelimiters = {} ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters) self.db = session self.ratelimiters = ratelimiters self.hasher = CryptContext( schemes=[ "argon2", "bcrypt_sha256", "bcrypt", "django_bcrypt", "unix_disabled", ], deprecated=["auto"], truncate_error=True, # Argon 2 Configuration argon2__memory_cost=1024, argon2__parallelism=6, argon2__time_cost=6, ) self._metrics = metrics @functools.lru_cache() def get_user(self, userid): # TODO: We probably don't actually want to just return the database # object here. # TODO: We need some sort of Anonymous User. return self.db.query(User).get(userid) @functools.lru_cache() def get_user_by_username(self, username): user_id = self.find_userid(username) return None if user_id is None else self.get_user(user_id) @functools.lru_cache() def get_user_by_email(self, email): user_id = self.find_userid_by_email(email) return None if user_id is None else self.get_user(user_id) @functools.lru_cache() def find_userid(self, username): try: user = self.db.query( User.id).filter(User.username == username).one() except NoResultFound: return return user.id @functools.lru_cache() def find_userid_by_email(self, email): try: # flake8: noqa user_id = (self.db.query( Email.user_id).filter(Email.email == email).one())[0] except NoResultFound: return return user_id def check_password(self, userid, password, *, tags=None): tags = tags if tags is not None else [] self._metrics.increment("warehouse.authentication.start", tags=tags) # The very first thing we want to do is check to see if we've hit our # global rate limit or not, assuming that we've been configured with a # global rate limiter anyways. if not self.ratelimiters["global"].test(): logger.warning("Global failed login threshold reached.") self._metrics.increment( "warehouse.authentication.ratelimited", tags=tags + ["ratelimiter:global"], ) raise TooManyFailedLogins( resets_in=self.ratelimiters["global"].resets_in()) user = self.get_user(userid) if user is not None: # Now, check to make sure that we haven't hitten a rate limit on a # per user basis. if not self.ratelimiters["user"].test(user.id): self._metrics.increment( "warehouse.authentication.ratelimited", tags=tags + ["ratelimiter:user"], ) raise TooManyFailedLogins( resets_in=self.ratelimiters["user"].resets_in(user.id)) # Actually check our hash, optionally getting a new hash for it if # we should upgrade our saved hashed. ok, new_hash = self.hasher.verify_and_update( password, user.password) # First, check to see if the password that we were given was OK. if ok: # Then, if the password was OK check to see if we've been given # a new password hash from the hasher, if so we'll want to save # that hash. if new_hash: user.password = new_hash self._metrics.increment("warehouse.authentication.ok", tags=tags) return True else: self._metrics.increment( "warehouse.authentication.failure", tags=tags + ["failure_reason:password"], ) else: self._metrics.increment("warehouse.authentication.failure", tags=tags + ["failure_reason:user"]) # If we've gotten here, then we'll want to record a failed login in our # rate limiting before returning False to indicate a failed password # verification. if user is not None: self.ratelimiters["user"].hit(user.id) self.ratelimiters["global"].hit() return False def create_user(self, username, name, password, is_active=False, is_superuser=False): user = User( username=username, name=name, password=self.hasher.hash(password), is_active=is_active, is_superuser=is_superuser, ) self.db.add(user) self.db.flush() # flush the db now so user.id is available return user def add_email(self, user_id, email_address, primary=None, verified=False): user = self.get_user(user_id) # If primary is None, then we're going to auto detect whether this should be the # primary address or not. The basic rule is that if the user doesn't already # have a primary address, then the address we're adding now is going to be # set to their primary. if primary is None: primary = True if user.primary_email is None else False email = Email(email=email_address, user=user, primary=primary, verified=verified) self.db.add(email) self.db.flush() # flush the db now so email.id is available return email def update_user(self, user_id, **changes): user = self.get_user(user_id) for attr, value in changes.items(): if attr == PASSWORD_FIELD: value = self.hasher.hash(value) setattr(user, attr, value) # If we've given the user a new password, then we also want to unset the # reason for disable... because a new password means no more disabled # user. if PASSWORD_FIELD in changes: user.disabled_for = None return user def disable_password(self, user_id, reason=None): user = self.get_user(user_id) user.password = self.hasher.disable() user.disabled_for = reason def is_disabled(self, user_id): user = self.get_user(user_id) # User is not disabled. if self.hasher.is_enabled(user.password): return (False, None) # User is disabled. else: return (True, user.disabled_for)
BadSignature, SignatureExpired) import os # Password/hash management from passlib.context import CryptContext # SQLAlchemy extension to map classes to database tables from sqlalchemy.ext.declarative import declarative_base # SQLAlchemy database table column type and related column data types from sqlalchemy import (Column, Enum, ForeignKey, Integer, String, Text) # For foreign key relationships and mapper from sqlalchemy.orm import relationship # SQLAlchemy module to connect to underlying database from sqlalchemy import create_engine # Globals # Hashing PasswordContext = CryptContext(schemes=['bcrypt'], deprecated='auto') # Random Secret Key # Note - drawback of this approach is everything based on it becomes invalid upon # system restart # Choose 32 as using SHA256 so want 256 bits of entropy (32 * 8) SecretKey = os.urandom(32) # SQLAlchemy setup - this is the base class that our model classes will be # derived from Base = declarative_base() # To do - Ideally create another class to represent providers (Google, Facebook, # Microsoft, ...); a user can then optionally be linked to one or more providers class User(Base): # DB table name __tablename__ = 'user'
import datetime import hashlib import hmac import uuid from passlib.context import CryptContext import jwt from .dict_helpers import inverted PASSLIB_CONTEXT = CryptContext( schemes=["pbkdf2_sha256"], default="pbkdf2_sha256", all__vary_rounds=0.1, # Lower number of rounds because we still want tests to finish in seconds: :) pbkdf2_sha256__default_rounds=1000, ) class JWTError(RuntimeError): pass def _decode_token(token, secret_key): """ Returns the payload decoded from the given token using the given secret key. """ try: payload = jwt.decode(
class Client: """ Authentication Client """ def __init__(self): self.db = DBHandler(db='yubiauth') self.pwd_context = CryptContext(**settings['CRYPT_CONTEXT']) if settings['USE_NATIVE_YKVAL']: # Native verify from .ykval import Validator self.ykval_client = Validator() else: # Using yubico_client to verify against remote server from yubico_client import Yubico self.ykval_client = Yubico(settings['YKVAL_CLIENT_ID'], settings['YKVAL_CLIENT_SECRET'], api_urls=settings['YKVAL_SERVERS']) def _get_user_info(self, username): """ Get user from DB Args: username Returns: dictionary of user data Raises: AuthFail if user does not exist """ user = self.db.get_user(username) if not user: raise YKAuthError('UNKNOWN_USER') logger.debug('[%s] Found user: %s', username, user) return user def _check_token(self, user, token_id): """ Check Token association with user Args: user: User data dict as recieved from _get_user_info() token_id: Token prefix (aka. publicname) Returns: None Raises: AuthFail if token is not associated with the user AithFail if token is disabled """ token = self.db.get_token(user['users_id'], token_id) if not token: logger.error('[%s] Token %s is not associated with user', user['users_name'], token_id) raise YKAuthError('INVALID_TOKEN') logger.debug('[%s] Found token: %s', user['users_name'], token) if not token.get('yubikeys_enabled'): logger.error('[%s] Token %s is disabled for %s', user['users_name'], token_id, user['users_name']) raise YKAuthError('DISABLED_TOKEN') def _validate_password(self, user, password): """ Validate password against the hash in SQL """ valid, new_hash = self.pwd_context.verify_and_update( str(password), user['users_auth']) if not valid: logger.error('[%(users_name)s] Invalid password', user) raise YKAuthError('BAD_PASSWORD') if new_hash: # TODO: update user's hash with new_hash logger.warning('[%(users_name)s] User password hash needs update', user) return True def authenticate(self, username, password, otp): """ Yubistack user authentication Args: username: Username of the user password: Password/PIN of the user otp: Yubikey one time password Returns: dict of authentication data Authentication process: 1. Check if token is enabled 2. Check if token is associated with the user & enabled 3. Validate users password 4. Validate OTP (YKVal) """ token_id = otp[:-TOKEN_LEN] # STEP 1: Check if token is enabled user = self._get_user_info(username) # STEP 2: Check if token is associated with the user & enabled self._check_token(user, token_id) # STEP 3: Validate users password self._validate_password(user, password) # STEP 4: Validate OTP self.ykval_client.verify(otp) return True
from anom import Model, props from markdown import markdown from passlib.context import CryptContext from slugify import slugify # [START password-property] ctx = CryptContext(schemes=["sha256_crypt"]) class Password(props.String): def validate(self, value): return ctx.hash(super().validate(value)) # [END password-property] # [START user-model] class User(Model, poly=True): username = props.String(indexed=True) password = Password() created_at = props.DateTime(indexed=True, auto_now_add=True) updated_at = props.DateTime(indexed=True, auto_now=True) @classmethod def login(cls, username, password): user = User.query().where(User.username == username).get() if user is None: return None if not ctx.verify(password, user.password): return None
def init_app(self, app): """Lazy initializer which takes an `app` and sets up the internal context.""" self.pwd_context = CryptContext( schemes=app.config['PASSLIB_SCHEMES'], )
"""Extensions registry All extensions here are used as singletons and initialized in application factory """ from flask_sqlalchemy import SQLAlchemy from passlib.context import CryptContext from flask_jwt_extended import JWTManager from flask_marshmallow import Marshmallow from flask_migrate import Migrate from neuroapi.commons.apispec import APISpecExt db = SQLAlchemy() jwt = JWTManager() ma = Marshmallow() migrate = Migrate() apispec = APISpecExt() pwd_context = CryptContext(schemes=['pbkdf2_sha256'], deprecated='auto')
import logging from passlib.context import CryptContext import openerp from openerp.osv import fields, osv _logger = logging.getLogger(__name__) default_crypt_context = CryptContext( # kdf which can be verified by the context. The default encryption kdf is # the first of the list ['pbkdf2_sha512', 'md5_crypt'], # deprecated algorithms are still verified as usual, but ``needs_update`` # will indicate that the stored hash should be replaced by a more recent # algorithm. Passlib 1.6 supports an `auto` value which deprecates any # algorithm but the default, but Debian only provides 1.5 so... deprecated=['md5_crypt'], ) class res_users(osv.osv): _inherit = "res.users" def init(self, cr): _logger.info( "Hashing passwords, may be slow for databases with many users...") cr.execute("SELECT id, password FROM res_users" " WHERE password IS NOT NULL" " AND password != ''") for uid, pwd in cr.fetchall():
def __init__(self): """ Init Config instance """ self.cache = CacheClass() if self.config_check_enabled: self._config_check() # define directories data_dir = os.path.normpath(self.data_dir) self.data_dir = data_dir # Try to decode certain names which allow unicode self._decode() # After that, pre-compile some regexes self.cache.item_dict_regex = re.compile(self.item_dict_regex, re.UNICODE) self.cache.item_group_regex = re.compile(self.item_group_regex, re.UNICODE) # the ..._regexact versions only match if nothing is left (exact match) self.cache.item_dict_regexact = re.compile( '^{0}$'.format(self.item_dict_regex), re.UNICODE) self.cache.item_group_regexact = re.compile( '^{0}$'.format(self.item_group_regex), re.UNICODE) # compiled functions ACL self.cache.acl_functions = AccessControlList( [self.acl_functions], valid=self.acl_rights_functions) plugins._loadPluginModule(self) if self.user_defaults[TIMEZONE] is None: self.user_defaults[TIMEZONE] = self.timezone_default if self.user_defaults[THEME_NAME] is None: self.user_defaults[THEME_NAME] = self.theme_default # Note: do not assign user_defaults['locale'] = locale_default # to give browser language detection a chance. try: self.language_default = parse_locale(self.locale_default)[0] except ValueError: raise error.ConfigurationError( "Invalid locale_default value (give something like 'en_US').") # post process self.auth_can_logout = [] self.auth_login_inputs = [] found_names = [] for auth in self.auth: if not auth.name: raise error.ConfigurationError( "Auth methods must have a name.") if auth.name in found_names: raise error.ConfigurationError( "Auth method names must be unique.") found_names.append(auth.name) if auth.logout_possible and auth.name: self.auth_can_logout.append(auth.name) for input in auth.login_inputs: if input not in self.auth_login_inputs: self.auth_login_inputs.append(input) self.auth_have_login = len(self.auth_login_inputs) > 0 self.auth_methods = found_names # internal dict for plugin 'modules' lists self._site_plugin_lists = {} # check if mail is possible and set flag: self.mail_enabled = (self.mail_smarthost is not None or self.mail_sendmail is not None) and self.mail_from self.mail_enabled = self.mail_enabled and True or False if self.namespace_mapping is None: raise error.ConfigurationError( "No storage configuration specified! You need to define a namespace_mapping." ) if self.backend_mapping is None: raise error.ConfigurationError( "No storage configuration specified! You need to define a backend_mapping." ) if self.acl_mapping is None: raise error.ConfigurationError( "No acl configuration specified! You need to define a acl_mapping." ) if self.secrets is None: # admin did not setup a real secret raise error.ConfigurationError( "No secret configured! You need to set secrets = 'somelongsecretstring' in your wiki config." ) if self.interwikiname is None: # admin did not setup a real interwikiname raise error.ConfigurationError( "No interwikiname configured! " "You need to set interwikiname = u'YourUniqueStableInterwikiName' in your wiki config." ) secret_key_names = [ 'security/ticket', ] secret_min_length = 10 if isinstance(self.secrets, str): if len(self.secrets) < secret_min_length: raise error.ConfigurationError( "The secrets = '...' wiki config setting is a way too short string " "(minimum length is {0} chars)!".format(secret_min_length)) # for lazy people: set all required secrets to same value secrets = {} for key in secret_key_names: secrets[key] = self.secrets self.secrets = secrets # we check if we have all secrets we need and that they have minimum length for secret_key_name in secret_key_names: try: secret = self.secrets[secret_key_name] if len(secret) < secret_min_length: raise ValueError except (KeyError, ValueError): raise error.ConfigurationError( "You must set a (at least {0} chars long) secret string for secrets['{1}']!" .format(secret_min_length, secret_key_name)) from passlib.context import CryptContext try: self.cache.pwd_context = CryptContext(**self.passlib_crypt_context) except ValueError as err: raise error.ConfigurationError( "passlib_crypt_context configuration is invalid [{0}].".format( err))
""" Various utils """ import uuid from flask import request, abort, jsonify from passlib.context import CryptContext import logging PASSWORD_CONTEXT = CryptContext(schemes=["pbkdf2_sha256"], default="pbkdf2_sha256", pbkdf2_sha256__default_rounds=300) class RequestHandler(object): def __init__(self): self.logger = logging.getLogger(type(self).__name__) self.set_context(self.get_request_attr("context")) def set_context(self, context): self.context = context def encrypt_password(self, password): return PASSWORD_CONTEXT.encrypt(password) def check_encrypted_password(self, password, hashed): return PASSWORD_CONTEXT.verify(password, hashed) def ensure_request_attr_container(self): if (not hasattr(request, "flaskdms")): setattr(request, "flaskdms", {}) def set_request_attr(self, key, value): self.ensure_request_attr_container()
from passlib.context import CryptContext from models.jwtUser import JWTUser from datetime import datetime, timedelta from utils.const import JWT_EXPIRATION_TIME_MINUTES, JWT_ALGORITHM, JWT_SECRET_KEY import jwt from fastapi import Depends from fastapi.security import OAuth2PasswordBearer import time import service.JWTUserService as JWTUserService passwordContext = CryptContext(schemes=["bcrypt"]) oauthSchema = OAuth2PasswordBearer(tokenUrl="/token") def getHashedPassword(password): return passwordContext.hash(password) def verifyPassword(password, hashPassword): try: return passwordContext.verify(password, hashPassword) except Exception as e: return False # Authenticate username and password to give JWT token async def authenticateUser(user: JWTUser): validUser = await JWTUserService.getUserByEmail(user) if validUser != {} and verifyPassword(user.password, validUser["password"]):
def get_oauth_password(): pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") return api_auth.get_password_hash(pwd_context, DOCS_PASSWORD)
try: import configparser as ConfigParser except ImportError: import ConfigParser import errno import logging import optparse import os import sys import odoo from .. import release, conf, loglevels from . import appdirs from passlib.context import CryptContext crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'], deprecated=['plaintext']) class MyOption (optparse.Option, object): """ optparse Option with two additional attributes. The list of command line options (getopt.Option) is used to create the list of the configuration file options. When reading the file, and then reading the command line arguments, we don't want optparse.parse results to override the configuration file values. But if we provide default values to optparse, optparse will return them and we can't know if they were really provided by the user or not. A solution is to not use optparse's default attribute, but use a custom one (that will be copied to create the default values of the configuration file). """ def __init__(self, *opts, **attrs):
def hash_password(password): crypt_context = CryptContext(schemes=['bcrypt_sha256']) return crypt_context.hash(password)
DEFAULT_ENTROPY = 32 # We use a passlib CryptContext to define acceptable hashing algorithms for # passwords. This allows us to easily # # - migrate to new hashing algorithms # - update the number of rounds used when hashing passwords # # simply by using the verify_and_update method of the CryptContext object. See # the passlib documentation on hash migration for more details: # # https://pythonhosted.org/passlib/lib/passlib.context-tutorial.html#context-migration-example # password_context = CryptContext(schemes=["bcrypt"], bcrypt__ident="2b", bcrypt__min_rounds=12) def derive_key(key_material, salt, info): """ Derive a fixed-size (64-byte) key for use in cryptographic operations. The key is derived using HKDF with the SHA-512 hash function. See https://tools.ietf.org/html/rfc5869. :type key_material: str or bytes :type salt: bytes :type info: bytes """ if not isinstance(key_material, bytes):
def verify_password(password, pwd_hash): crypt_context = CryptContext(schemes=['bcrypt_sha256']) return crypt_context.verify(password, pwd_hash)
class User(DatastoreModel): """Neptune users.""" # tl;dr: values forced to lower case before storage here AND in # uniqueness_key()! # # Emails are stored in two places and must match across them to make # sure we don't get duplicate users: in User.email and the key name of # the corresponding Unique entity (see uniqueness_key()). And because # email addresses are effectively case insensitive while our databases # are case sensistive, force them all to lower case before storage. # Because people _do_ vary their capitalization between sessions. See # #387. email = ndb.StringProperty(required=True, validator=lambda prop, value: value.lower()) name = ndb.StringProperty() role = ndb.StringProperty() phone_number = ndb.StringProperty() hashed_password = ndb.StringProperty() google_id = ndb.StringProperty() # user type can be: super_admin, program_admin, user, public user_type = ndb.StringProperty(default='user') # notification option has two possible keys: # { # "email": ("yes"|"no"), # "sms": ("yes"|"no") # } notification_option_json = ndb.TextProperty(default=r'{}') owned_organizations = ndb.StringProperty(repeated=True) assc_organizations = ndb.StringProperty(repeated=True) owned_programs = ndb.StringProperty(repeated=True) owned_projects = ndb.StringProperty(repeated=True) assc_projects = ndb.StringProperty(repeated=True) owned_data_tables = ndb.StringProperty(repeated=True) owned_data_requests = ndb.StringProperty(repeated=True) last_login = ndb.DateTimeProperty() # App Engine can only run pure-python external libraries, and so we can't get # a native (C-based) implementation of bcrypt. Pure python implementations are # so slow that [the feasible number of rounds is insecure][1]. This uses the # [algorithm recommended by passlib][2]. # [1]: http://stackoverflow.com/questions/7027196/how-can-i-use-bcrypt-scrypt-on-appengine-for-python # [2]: https://pythonhosted.org/passlib/new_app_quickstart.html#sha512-crypt password_hashing_context = CryptContext( schemes=['sha512_crypt', 'sha256_crypt'], default='sha512_crypt', all__vary_rounds=0.1, # Can change hashing rounds here. 656,000 is the default. # sha512_crypt__default_rounds=656000, # sha256_crypt__default_rounds=656000, ) json_props = ['notification_option_json'] @property def super_admin(self): return self.user_type == 'super_admin' @property def non_admin(self): # Matches either value while we transition. See #985. return self.user_type in ('org_admin', 'user') @property def notification_option(self): return (json.loads(self.notification_option_json) if self.notification_option_json else None) @notification_option.setter def notification_option(self, obj): self.notification_option_json = json.dumps(obj) return obj @classmethod def create(klass, **kwargs): # Create Unique entity based on email, allowing strongly consistent # prevention of duplicates. is_unique_email = Unique.create(User.uniqueness_key(kwargs['email'])) if not is_unique_email: raise DuplicateUser( "There is already a user with email {}.".format( kwargs['email'])) return super(klass, klass).create(**kwargs) @classmethod def create_public(klass): return super(klass, klass).create( id='public', name='public', email='public', user_type='public', ) @classmethod def uniqueness_key(klass, email): # See #387. return u'User.email:{}'.format(email.lower()) @classmethod def email_exists(klass, email): """Test if this email has been registered, idempotent.""" return Unique.get_by_id(User.uniqueness_key(email)) is not None @classmethod def get_by_auth(klass, auth_type, auth_id): # All stored emails are in lower case. If we hope to find them, need # to lower case the search param. See #387. if auth_type == 'email': auth_id = auth_id.lower() matches = User.get(order='created', **{auth_type: auth_id}) if len(matches) == 0: return None elif len(matches) == 1: return matches[0] elif len(matches) > 1: logging.error( u"More than one user matches auth info: {}, {}.".format( auth_type, auth_id)) # We'll let the function pass on and take the first of multiple # duplicate users, which will be the earliest-created one. return matches[0] @classmethod def property_types(klass): """Overrides DatastoreModel. Prevents hashed_password from being set.""" props = super(klass, klass).property_types() props.pop('hashed_password', None) return props @classmethod def example_params(klass): name = ''.join(random.choice(string.ascii_uppercase) for c in range(3)) return { 'name': name, 'email': name + '@example.com', 'phone_number': '+1 (555) 555-5555', 'hashed_password': '******', 'user_type': random.choice(['user', 'program_admin', 'super_admin']), } @classmethod def hash_password(klass, password): if re.match(config.password_pattern, password) is None: raise BadPassword(u'Bad password: {}'.format(password)) return klass.password_hashing_context.encrypt(password) @classmethod def verify_password(klass, password, hashed_password): return (klass.password_hashing_context.verify( password, hashed_password) if hashed_password else False) def __nonzero__(self): return False if getattr(self, 'user_type', None) == 'public' else True def before_put(self, *args, **kwargs): if self.user_type == 'public': raise Exception("Public user cannot be saved.") def to_client_dict(self, **kwargs): """Overrides DatastoreModel, modifies behavior of hashed_password. Change hashed_password to a boolean so client can detect if a user hasn't set their password yet. Also prevent hash from be unsafely exposed. """ output = super(User, self).to_client_dict() output['hashed_password'] = bool(self.hashed_password) return output def create_reset_link(self, domain, token, continue_url='', case=''): """Create the kind of jwt-based set password link used by Triton. Args: domain: str, beginning with protocol, designed this way to make it easier to switch btwn localhost on http and deployed on https. continue_url: str, page should support redirecting user to this url after successful submission case: str, either 'reset' or 'invitation', aids the UI in displaying helpful text based on why the user has arrived. """ return util.set_query_parameters( '{}/set_password/{}'.format(domain, token), continue_url=continue_url, case=case, )
def crypt_context(): return CryptContext(schemes=['sha256_crypt'])
def init_app(self, app, user_class, is_blacklisted=None): """ Initializes the Praetorian extension :param: app: The flask app to bind this extension to :param: user_class: The class used to interact with user data :param: is_blacklisted: A method that may optionally be used to check the token against a blacklist when access or refresh is requested Should take the jti for the token to check as a single argument. Returns True if the jti is blacklisted, False otherwise. By default, always returns False. """ PraetorianError.require_condition( app.config.get('SECRET_KEY') is not None, "There must be a SECRET_KEY app config setting set", ) self.hash_autoupdate = app.config.get( 'PRAETORIAN_HASH_AUTOUPDATE', DEFAULT_HASH_AUTOUPDATE, ) self.hash_autotest = app.config.get( 'PRAETORIAN_HASH_AUTOTEST', DEFAULT_HASH_AUTOTEST, ) self.pwd_ctx = CryptContext( schemes=app.config.get('PRAETORIAN_HASH_ALLOWED_SCHEMES', DEFAULT_HASH_ALLOWED_SCHEMES), default=app.config.get('PRAETORIAN_HASH_SCHEME', DEFAULT_HASH_SCHEME), deprecated=app.config.get('PRAETORIAN_HASH_DEPRECATED_SCHEMES', DEFAULT_HASH_DEPRECATED_SCHEMES), ) valid_schemes = self.pwd_ctx.schemes() PraetorianError.require_condition( self.hash_scheme in valid_schemes or self.hash_scheme is None, "If {} is set, it must be one of the following schemes: {}", 'PRAETORIAN_HASH_SCHEME', valid_schemes, ) self.user_class = self._validate_user_class(user_class) self.is_blacklisted = is_blacklisted or (lambda t: False) self.encode_key = app.config['SECRET_KEY'] self.allowed_algorithms = app.config.get( 'JWT_ALLOWED_ALGORITHMS', DEFAULT_JWT_ALLOWED_ALGORITHMS, ) self.encode_algorithm = app.config.get( 'JWT_ALGORITHM', DEFAULT_JWT_ALGORITHM, ) self.access_lifespan = app.config.get( 'JWT_ACCESS_LIFESPAN', DEFAULT_JWT_ACCESS_LIFESPAN, ) self.refresh_lifespan = app.config.get( 'JWT_REFRESH_LIFESPAN', DEFAULT_JWT_REFRESH_LIFESPAN, ) self.reset_lifespan = app.config.get( 'JWT_RESET_LIFESPAN', DEFAULT_JWT_RESET_LIFESPAN, ) self.header_name = app.config.get( 'JWT_HEADER_NAME', DEFAULT_JWT_HEADER_NAME, ) self.header_type = app.config.get( 'JWT_HEADER_TYPE', DEFAULT_JWT_HEADER_TYPE, ) self.user_class_validation_method = app.config.get( 'USER_CLASS_VALIDATION_METHOD', DEFAULT_USER_CLASS_VALIDATION_METHOD, ) self.confirmation_template = app.config.get( 'PRAETORIAN_CONFIRMATION_TEMPLATE', DEFAULT_CONFIRMATION_TEMPLATE, ) self.confirmation_uri = app.config.get('PRAETORIAN_CONFIRMATION_URI', ) self.confirmation_sender = app.config.get( 'PRAETORIAN_CONFIRMATION_SENDER', ) self.confirmation_subject = app.config.get( 'PRAETORIAN_CONFIRMATION_SUBJECT', DEFAULT_CONFIRMATION_SUBJECT, ) self.reset_template = app.config.get( 'PRAETORIAN_RESET_TEMPLATE', DEFAULT_RESET_TEMPLATE, ) self.reset_uri = app.config.get('PRAETORIAN_RESET_URI', ) self.reset_sender = app.config.get('PRAETORIAN_RESET_SENDER', ) self.reset_subject = app.config.get( 'PRAETORIAN_RESET_SUBJECT', DEFAULT_RESET_SUBJECT, ) if isinstance(self.access_lifespan, dict): self.access_lifespan = pendulum.duration(**self.access_lifespan) elif isinstance(self.access_lifespan, str): self.access_lifespan = duration_from_string(self.access_lifespan) ConfigurationError.require_condition( isinstance(self.access_lifespan, datetime.timedelta), "access lifespan was not configured", ) if isinstance(self.refresh_lifespan, dict): self.refresh_lifespan = pendulum.duration(**self.refresh_lifespan) if isinstance(self.refresh_lifespan, str): self.refresh_lifespan = duration_from_string(self.refresh_lifespan) ConfigurationError.require_condition( isinstance(self.refresh_lifespan, datetime.timedelta), "refresh lifespan was not configured", ) if not app.config.get('DISABLE_PRAETORIAN_ERROR_HANDLER'): app.register_error_handler( PraetorianError, PraetorianError.build_error_handler(), ) self.is_testing = app.config.get('TESTING', False) if not hasattr(app, 'extensions'): app.extensions = {} app.extensions['praetorian'] = self return app