Search code examples
pythoneve

In Eve, how can you store the user's password securely?


If you put a debugger in the run file, you will see that the user's password is hashed, but when you look in the mongo collection, the user's password is stored in plain text. How do you save the user's password as a hash?

Here are my files:

run.py:

from eve import Eve
from eve.auth import BasicAuth

import bcrypt

class BCryptAuth(BasicAuth):
    def check_auth(self, username, password, allowed_roles, resource, method):
        # use Eve's own db driver; no additional connections/resources are used
        accounts = app.data.driver.db["accounts"]
        account = accounts.find_one({"username": username})
        return account and \
            bcrypt.hashpw(password, account['password']) == account['password']

def create_user(*arguments, **keywords):
    password = arguments[0][0]['password']
    username = arguments[0][0]['username']
    user = {
        "password": bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()),
        "username": username,
    }
    return post_internal("accounts", user)


app = Eve(auth=BCryptAuth)
app.on_insert_accounts += create_user

if __name__ == '__main__':
    app.run()

settings.py:

API_NAME = "gametest"

CACHE_CONTROL = "max-age=20"
CACHE_EXPIRES = 20
MONGO_DBNAME = "gametest"
MONGO_HOST = "localhost"
MONGO_PORT = 27017
PUBLIC_ITEM_METHODS = ["GET"]
RESOURCE_METHODS = ["GET"]

accounts_schema = {
    "username": {
        "type": "string",
        "required": True,
        "unique": True,
    },
    "password": {
        "type": "string",
        "required": True,
    },
}

accounts = {
    # the standard account entry point is defined as
    # '/accounts/<ObjectId>'. We define  an additional read-only entry
    # point accessible at '/accounts/<username>'.
    "additional_lookup": {
        "url": "regex('[\w]+')",
        "field": "username",
    },

    # We also disable endpoint caching as we don't want client apps to
    # cache account data.
    "cache_control": "",
    "cache_expires": 0,

    # Finally, let's add the schema definition for this endpoint.
    "schema": accounts_schema,
    "public_methods": ["POST"],
    "resource_methods": ["POST"],
}
games_schema = {
    "game_id": {
        "type": "objectid",
        "required": True
    },
    "title": {
        "type": "string",
        "required": True
    },
}

games = {
    "item_title": "game",
    "schema": games_schema,
}

orders = {
    "schema": {
        "game": {
            "type": "objectid",
            "required": True,
        },
    },
    "resource_methods": ["GET", "POST"],
}

DOMAIN = {
    "accounts", accounts,
    "orders": orders,
    "games": game,
}

Solution

  • There were a few major things in your run.py that were preventing you from authenticating:

    • In your create_user event hook you were generating a salt with bcrypt.gensalt(), but you weren't saving the salt anywhere. Salts are useful for preventing rainbow table attacks, but you need to save them so that when you try to hash the password again you get the same result.
    • You're using the on_insert_accounts event hook to modify the document before it's posted, but then returning a post_internal instead of letting the event hook run its course. This might work, but I feel like you should just use the event hook as it was intended.

    Here is the modified run.py:

    from eve import Eve
    from eve.auth import BasicAuth
    
    import bcrypt
    
    class BCryptAuth(BasicAuth):
        def check_auth(self, username, password, allowed_roles, resource, method):
            # use Eve's own db driver; no additional connections/resources are used
            accounts = app.data.driver.db["accounts"]
            account = accounts.find_one({"username": username})
            return account and \
                bcrypt.hashpw(password.encode('utf-8'), account['salt'].encode('utf-8')) == account['password']
    
    def create_user(documents):
        for document in documents:
            document['salt'] = bcrypt.gensalt().encode('utf-8')
            password = document['password'].encode('utf-8')
            document['password'] = bcrypt.hashpw(password, document['salt'])
    
    app = Eve(auth=BCryptAuth)
    app.on_insert_accounts += create_user
    
    if __name__ == '__main__':
        app.run()
    

    There were a few typos in your settings.py, so I'm including a working version here for good measure:

    API_NAME = "gametest"
    
    CACHE_CONTROL = "max-age=20"
    CACHE_EXPIRES = 20
    MONGO_DBNAME = "gametest"
    MONGO_HOST = "localhost"
    MONGO_PORT = 27017
    PUBLIC_ITEM_METHODS = ["GET"]
    RESOURCE_METHODS = ["GET"]
    
    accounts_schema = {
        "username": {
            "type": "string",
            "required": True,
            "unique": True
        },
        "password": {
            "type": "string",
            "required": True
        }
    }
    
    accounts = {
        # the standard account entry point is defined as
        # '/accounts/<ObjectId>'. We define  an additional read-only entry
        # point accessible at '/accounts/<username>'.
        "additional_lookup": {
            "url": "regex('[\w]+')",
            "field": "username",
        },
    
        # We also disable endpoint caching as we don't want client apps to
        # cache account data.
        "cache_control": "",
        "cache_expires": 0,
    
        # Finally, let's add the schema definition for this endpoint.
        "schema": accounts_schema,
        "public_methods": ["POST"],
        "resource_methods": ["POST"]
    }
    games_schema = {
        "game_id": {
            "type": "objectid",
            "required": True
        },
        "title": {
            "type": "string",
            "required": True
        }
    }
    
    games = {
        "item_title": "game",
        "schema": games_schema
    }
    
    orders = {
        "schema": {
            "game": {
                "type": "objectid",
                "required": True,
            }
        },
        "resource_methods": ["GET", "POST"]
    }
    
    DOMAIN = {
        "accounts": accounts,
        "orders": orders,
        "games": games
    }