def authenticate(passwordFile: HtpasswdFile, credentials: Credentials) -> None: """Checks a name and password combination against a password file. Returns if authentication succeeds. Raises `UnauthorizedLogin` if the given user name and password combination is not accepted. The hashed version of the password will be updated with a new hash function if the current one is depricated. """ name = credentials.name password = credentials.password try: checkPassword(password) except ValueError as ex: raise UnauthorizedLogin(ex) from ex # Have passlib check the password. When passed a None hash, it will # perform a dummy computation to keep the timing consistent. passwordFile.load_if_changed() correct, newHash = passwordFile.context.verify_and_update( password.encode(), passwordFile.get_hash(name)) if not correct: raise UnauthorizedLogin('Authentication failed') if newHash is not None: # Replace hash with better algorithm or config. passwordFile.set_hash(name, newHash) writePasswordFile(passwordFile)
def verify_passwd_hash(username, password): htContent = HtpasswdFile(app.config['HTPASSWD_FILE'], default_scheme='sha256_crypt') passwdHash = htContent.get_hash(username) try: hashMatch = sha256_crypt.verify(password, passwdHash) if hashMatch: return True except ValueError: return False except TypeError: return False
class __FileAuthenticator: '''Takes a htpasswd file path from the HTPASSWD_FILE environment variable. ''' def __init__(self, filename): self.filename = filename self.data = HtpasswdFile(self.filename) def authenticate(self, username, password): """ Returns True if username exists and password is valid, False otherwise """ if self.data.check_password(username, password): # Note: check_password returns None if username does not exists return True return False def get_hash(self, username): """ Returns a hash if username exists, None otherwise """ return self.data.get_hash(username) def find_user(self, username): """ Returns True if usename exists in db, False otherwise """ return username in self.data.users()
class HtPasswdAuth(object): """Configure htpasswd based basic and token authentication.""" def __init__(self, app=None): """Boiler plate extension init with log_level being declared""" self.users = HtpasswdFile() self.app = app if app is not None: self.init_app(app) def init_app(self, app): """ Find and configure the user database from specified file """ app.config.setdefault('FLASK_AUTH_ALL', False) app.config.setdefault('FLASK_AUTH_REALM', 'Login Required') # Default set to bad file to trigger IOError app.config.setdefault('FLASK_HTPASSWD_PATH', '/^^^/^^^') # Load up user database try: self.load_users(app) except IOError: log.critical( 'No htpasswd file loaded, please set `FLASK_HTPASSWD`' 'or `FLASK_HTPASSWD_PATH` environment variable to a ' 'valid apache htpasswd file.' ) # Allow requiring auth for entire app, with pre request method @app.before_request def require_auth(): # pylint: disable=unused-variable """Pre request processing for enabling full app authentication.""" if not current_app.config['FLASK_AUTH_ALL']: return is_valid, user = self.authenticate() if not is_valid: return self.auth_failed() g.user = user def load_users(self, app): """ Load users from configured file. Args: app (flask.Flask): Flask application to load users from. Raises: IOError: If the configured htpasswd file does not exist. Returns: None """ self.users = HtpasswdFile( app.config['FLASK_HTPASSWD_PATH'] ) def check_basic_auth(self, username, password): """ This function is called to check if a username / password combination is valid via the htpasswd file. """ valid = self.users.check_password( username, password ) if not valid: log.warning('Invalid login from %s', username) valid = False return ( valid, username ) @staticmethod def get_signature(): """ Setup crypto sig. """ return Serializer(current_app.config['FLASK_SECRET']) def get_hashhash(self, username): """ Generate a digest of the htpasswd hash """ return hashlib.sha256( self.users.get_hash(username) ).hexdigest() def generate_token(self, username): """ assumes user exists in htpasswd file. Return the token for the given user by signing a token of the username and a hash of the htpasswd string. """ serializer = self.get_signature() return serializer.dumps( { 'username': username, 'hashhash': self.get_hashhash(username) } ).decode('UTF-8') def check_token_auth(self, token): """ Check to see who this is and if their token gets them into the system. """ serializer = self.get_signature() try: data = serializer.loads(token) except BadSignature: log.warning('Received bad token signature') return False, None if data['username'] not in self.users.users(): log.warning( 'Token auth signed message, but invalid user %s', data['username'] ) return False, None if data['hashhash'] != self.get_hashhash(data['username']): log.warning( 'Token and password do not match, %s ' 'needs to regenerate token', data['username'] ) return False, None return True, data['username'] @staticmethod def auth_failed(): """ Sends a 401 response that enables basic auth """ return Response( 'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="{0}"'.format( current_app.config['FLASK_AUTH_REALM'] )} ) def authenticate(self): """Authenticate user by any means and return either true or false. Args: Returns: tuple (is_valid, username): True is valid user, False if not """ basic_auth = request.authorization is_valid = False user = None if basic_auth: is_valid, user = self.check_basic_auth( basic_auth.username, basic_auth.password ) else: # Try token auth token = request.headers.get('Authorization', None) param_token = request.args.get('access_token') if token or param_token: if token: # slice the 'token ' piece of the header (following # github style): token = token[6:] else: # Grab it from query dict instead token = param_token log.debug('Received token: %s', token) is_valid, user = self.check_token_auth(token) return (is_valid, user) def required(self, func): """ Decorator function with basic and token authentication handler """ @wraps(func) def decorated(*args, **kwargs): """ Actual wrapper to run the auth checks. """ is_valid, user = self.authenticate() if not is_valid: return self.auth_failed() kwargs['user'] = user return func(*args, **kwargs) return decorated
class HtPasswdAuth: """Configure htpasswd based basic and token authentication.""" def __init__(self, app=None): """Boiler plate extension init with log_level being declared""" self.users = HtpasswdFile() self.app = app if app is not None: self.init_app(app) def init_app(self, app): """ Find and configure the user database from specified file """ # pylint: disable=inconsistent-return-statements app.config.setdefault('FLASK_AUTH_ALL', False) app.config.setdefault('FLASK_AUTH_REALM', 'Login Required') # Default set to bad file to trigger IOError app.config.setdefault('FLASK_HTPASSWD_PATH', '/^^^/^^^') # Load up user database try: self.load_users(app) except IOError: log.critical('No htpasswd file loaded, please set `FLASK_HTPASSWD`' 'or `FLASK_HTPASSWD_PATH` environment variable to a ' 'valid apache htpasswd file.') # Allow requiring auth for entire app, with pre request method @app.before_request def require_auth(): # pylint: disable=unused-variable """Pre request processing for enabling full app authentication.""" if not current_app.config['FLASK_AUTH_ALL']: return None is_valid, user = self.authenticate() if not is_valid: return self.auth_failed() g.user = user def load_users(self, app): """ Load users from configured file. Args: app (flask.Flask): Flask application to load users from. Raises: IOError: If the configured htpasswd file does not exist. Returns: None """ self.users = HtpasswdFile(app.config['FLASK_HTPASSWD_PATH']) def check_basic_auth(self, username, password): """ This function is called to check if a username / password combination is valid via the htpasswd file. """ valid = self.users.check_password(username, password) if not valid: log.warning('Invalid login from %s', username) valid = False return (valid, username) @staticmethod def get_signature(): """ Setup crypto sig. """ return Serializer(current_app.config['FLASK_SECRET']) def get_hashhash(self, username): """ Generate a digest of the htpasswd hash """ return hashlib.sha256(self.users.get_hash(username)).hexdigest() def generate_token(self, username): """ assumes user exists in htpasswd file. Return the token for the given user by signing a token of the username and a hash of the htpasswd string. """ serializer = self.get_signature() return serializer.dumps({ 'username': username, 'hashhash': self.get_hashhash(username) }).decode('UTF-8') def check_token_auth(self, token): """ Check to see who this is and if their token gets them into the system. """ serializer = self.get_signature() try: data = serializer.loads(token) except BadSignature: log.warning('Received bad token signature') return False, None if data['username'] not in self.users.users(): log.warning('Token auth signed message, but invalid user %s', data['username']) return False, None if data['hashhash'] != self.get_hashhash(data['username']): log.warning( 'Token and password do not match, %s ' 'needs to regenerate token', data['username']) return False, None return True, data['username'] @staticmethod def auth_failed(): """ Sends a 401 response that enables basic auth """ if request.endpoint == "auth": r = make_response() r.status_code = 401 r.headers.set( 'WWW-Authenticate', 'basic realm="{0}"'.format( current_app.config['FLASK_AUTH_REALM'])) return r else: return abort(401) def authenticate(self): """Authenticate user by any means and return either true or false. Args: Returns: tuple (is_valid, username): True is valid user, False if not """ basic_auth = request.authorization is_valid = False user = None if basic_auth: is_valid, user = self.check_basic_auth(basic_auth.username, basic_auth.password) else: # Try token auth token = request.headers.get('Authorization', None) param_token = request.args.get('access_token') if token or param_token: if token: # slice the 'token ' piece of the header (following # github style): token = token[6:] else: # Grab it from query dict instead token = param_token log.debug('Received token: %s', token) is_valid, user = self.check_token_auth(token) return (is_valid, user) def required(self, func): """ Decorator function with basic and token authentication handler """ @wraps(func) def decorated(*args, **kwargs): """ Actual wrapper to run the auth checks. """ is_valid, user = self.authenticate() if not is_valid: return self.auth_failed() kwargs['user'] = user return func(*args, **kwargs) return decorated
def has_password(self, name): """Does an entry for the given user exist in the password file?""" passwordFile = HtpasswdFile(str(self.db_path / 'passwords')) return passwordFile.get_hash(name) is not None