"""Token Logic. Handles the token validation and generation. A token is a piece of information that allows a client (with its secret) to perform actions on a behalf of a userself. """ import random import hashids from sukimu.operations import Equal from auth.models.token import Token from auth.models.token import TYPE from auth.utils import date from auth.utils import response ERROR_MISSING_TOKEN = 'The token could not be found.' def create_code_token(login, user_id, client_id, user_ip=None): """Create a code token. The code token is the code that will be returned as part of the URL when the user is redirected to the service. This code will then allow the service to fetch the full token for the user. Args: login (str): the user's login. user_id (str): id of user from vend_contact or orchadmin_users table client_id (int): id of client user is requesting to connect to user_ip (str): ip of user logged in. Returns: Response: the request response (errors will be included if any. """ ten_minutes = 600 return create( TYPE.CODE, login, str(user_id), client_id, expires_in=ten_minutes, user_ip=user_ip) def create_connection_token(login, user_id, client_id, user_ip=None): """Create a connection token. Connection token are more persistent than other tokens, they allow the service to perform actions on the behalf of a user (versus the CODE token which only allows the service to get access to the CONNECTION token by hitting the /token endpoint). Args: login (str): the user's login. user_id (str): id of user from vend_contact or orchadmin_users table client_id (int): id of client user is requesting to connect to user_ip (str): ip of user logged in. Returns: Response: the request response (errors will be included if any. """ days_30 = 24 * 3600 * 30 return create( TYPE.CONNECTION, login, str(user_id), client_id, expires_in=days_30, user_ip=user_ip) def create_password_token( login, user_id, client_id, user_ip=None, expires_in=86400): """Create a lost password token. When created, Lost Password tokens are set to expire in 24 hours. This token will be used as part of the URL to allow the user to set a new password. Args: login (str): the user's login. user_id (str): id of user from vend_contact or orchadmin_users table client_id (int): id of client user is requesting to connect to user_ip (str): ip of user logged in. expires_in (int): time in seconds until the token expires Returns: Response: the request response (errors will be included if any). """ password_token = create( TYPE.LOST_PASSWORD, login, str(user_id), client_id, expires_in=expires_in, user_ip=user_ip) # TODO: send email to user using password_token return password_token def create(token_type, login, user_id, client_id, expires_in, user_ip=None): """Create a token for the user to use. Args: token_type (int): token being requested (CONNECTION, CODE, LOST_PASSWORD) login (str): the user's login. user_id (str): id of user from vend_contact or orchadmin_users table client_id (int): id of client user is requesting to connect to user_ip (str): ip of user logged in. Returns: Response: the request response (errors will be included if any.) """ today = int(date.now_utc_timestamp()) generated_token = generate_token(login, today) return Token.create( token=generated_token, type=token_type, date=today, revoked=False, expires_in=expires_in, client_id=client_id, user_id=str(user_id), user_login=login) def generate_token(login, time): """Generate a token for a user. Tokens should be easy to revert when you know a login (this allow to get back the time of when a token was generated, even before calling the db). Args: login (str): the user's token. time (int): current time, in int. Returns: str: the generated token. """ user_hash = hashids.Hashids(login) salt = int(random.random() * 10000) return user_hash.encode(time, salt) def validate(token, client_id, token_type): """Validate a token. Verifies if a token is valid or not based on the token id, login and client id. We also verify if the token has not expired. Args: token (str) User's token client_id (int): id of the service the user wants to connect to token_type (int): the type of the token. Returns: Response: the token if the data is valid. """ default_response = response.Response( errors={'token': 'The auth token is not valid.'}) if not token or not client_id: return default_response current_token = Token.fetch_one(token=Equal(token)) if not current_token.success or current_token.revoked: return default_response # check if token is active now = int(date.now_utc_timestamp()) is_active = current_token.date + current_token.expires_in > now # Additional checks is_client_id = current_token.client_id == client_id # Check the token type is the same. is_same_type = current_token.type == token_type if is_client_id and is_active and is_same_type: return current_token return default_response def validate_connection_token(token, client_id): """Validate a connection token. Args: token (str): User's token. client_id (int): id of the service the user wants to access. Returns: Response: the token if the data is valid. """ return validate(token, client_id, TYPE.CONNECTION) def validate_code_token(token, client_id): """Validate a code. Args: token (str): User's token. client_id (int): id of the service the user wants to access. Returns: Response: the token if the data is valid. """ return validate(token, client_id, TYPE.CODE) def validate_password_token(token): """Validate password token. Validating a password token is different from the other validation methods since no client is provided (the password recovery flow happens directly within the service). Args: token (str): User's token. Returns: Response: the token if the data is valid. """ default_error_response = response.Response( errors=dict(token='The auth token is not valid.')) if not token: return default_error_response current_token = Token.fetch_one(token=Equal(token)) if not current_token.success or current_token.revoked: return default_error_response now = int(date.now_utc_timestamp()) is_active = current_token.date + current_token.expires_in > now is_same_type = current_token.type == TYPE.LOST_PASSWORD if is_active and is_same_type: return current_token return default_error_response def revoke(token): """Revoke a user's token. Args: token(str): User's token. Returns: Response: The result of the update. """ current_token = Token.fetch_one(token=Equal(token)) if not current_token.success: return current_token if current_token.revoked: return response.Response( errors={'token': 'This token has already been revoked.'}) return Token.update(dict(token=token), revoked=True) def fetch_token(token): """Retrieve a token from the token string. Args: token (str): Token string. Returns: Response: The full token representation. """ current_token = Token.fetch_one(token=Equal(token)) if not current_token.success: return response.Response( errors=dict(token=ERROR_MISSING_TOKEN)) return current_token def update_token_expiration(token_id, expires_in=1200): """Find a token with the given identifier and update its expiration. Updating the token expiration includes first verifying the type of token (it only applies to tokens that are of type LOST_PASSWORD), and which have not been revoked. Assuming the token is still valid, expires_in should always be updated to be the sum of the desired expires_in and whatever time has elapsed since the token was created and when this request occurs. Example: • Token created at t0 has an initial expiration of 1200s (20 minutes). • 5 minutes have passed. • A request is made to update the expiration time to 2 minutes. • `expires_in` should be of 7 minutes (5 + 2). Args: token_id (str): Identifying values to specify the token expires_in (int): The length in seconds of the token expiration Returns: Response: The full token representation """ current_token = validate_password_token(token_id) if not current_token.success: return current_token now = int(date.now_utc_timestamp()) expires_in = now - current_token.date + expires_in return Token.update(dict(token=token_id), expires_in=expires_in)