From 7547ee362c13c245298e1c05b76f122cecd265d0 Mon Sep 17 00:00:00 2001 From: Dev Mukherjee Date: Tue, 25 Apr 2023 11:31:58 +1000 Subject: [PATCH] refactor: tidyup user and auth before REFS #51 --- src/labs/models/user.py | 48 ++++++++++++++++++++++++++++++++++++----- src/labs/utils/auth.py | 4 +++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/labs/models/user.py b/src/labs/models/user.py index da029d6..b5378a5 100644 --- a/src/labs/models/user.py +++ b/src/labs/models/user.py @@ -1,4 +1,9 @@ -"""An application user +""" An application user + + The aim is to provide a set of fields that are common to most + applications with a good know set of practices around authentication + both via password and one time passwords. + """ from typing import Optional @@ -62,18 +67,38 @@ class User( def check_password(self, plain_text_pass): return verify_password(plain_text_pass, self.password) - def get_otp(self, digits: int, timeout: int): - """ + def get_otp( + self, + digits: int = 6, + timeout: int = 30 + ): + """ Get the current OTP for the user + + This is sent to the user via email or SMS and is used to + authenticate the user. This should be different based + on the timeout and the digits. """ otp = TOTP(self.secret, digits=digits, interval=timeout) return otp.now() - def verify_otp(self, timeout: int, window: int, token: str): + def verify_otp( + self, + token: str, + timeout: int = 30, + window: int = 30 + ): + """ + """ otp = TOTP(self.secret, interval=timeout) return otp.verify(token, valid_window=window) @classmethod async def get_by_email(cls, session, email): + """ A custom getter where the user is found via email + + The aim is to assist with finding the user by email + which is handy when authenticating via passwords + """ query = cls._base_get_query().where(cls.email == email) try: results = await session.execute(query) @@ -113,6 +138,19 @@ def receive_init(target, args, kwargs): target.otp_secret = random_base32() def encrypt_password(target, value, oldvalue, initiator): + """ Encrypt the password when it is set + + This enables the application logic to simply set the plain + text password and the model encrypts it on the way in. + + The idea is to abstract this from the duties of the application. + """ return hash_password(value) -event.listen(User.password, 'set', encrypt_password, retval=True) +# Support for the above method to run when the password is set +event.listen( + User.password, + 'set', + encrypt_password, + retval=True +) diff --git a/src/labs/utils/auth.py b/src/labs/utils/auth.py index 91f9876..8cc492f 100644 --- a/src/labs/utils/auth.py +++ b/src/labs/utils/auth.py @@ -24,6 +24,8 @@ def verify_password( hashed_password ) -> bool: """ Use the crypt context to verify the password + + the str.encode is used to convert the string to bytes """ return bcrypt.checkpw( str.encode(plain_password), @@ -36,7 +38,7 @@ def hash_password(password) -> str: This is used by the setter in the User model to hash the password when the handlers set the property. - the input string has to be + the input string has to be an byte string """ encoded_password = bcrypt.hashpw( str.encode(password),