"""auth.py. jwt token generation and validation for m2m communication. """ from datetime import datetime from datetime import timedelta import json import random from typing import Optional from urllib.request import urlopen from warnings import warn from cachelib.uwsgi import UWSGICache from jwtauth import JWTAuth from jwtauth.exceptions import JWTAuthError from jwtauth.utils import jwt_auth_from_environment from owsresponse import response from owsresponse import status as response_status from retrying import retry from secrets_manager.python_ext import PythonSecretsManager from owsrequest import config from owsrequest.config import logger from owsrequest.constants import auth as auth_constants from owsrequest.constants import errors from owsrequest.utils import uwsgi_cache_refresh_threads def get_auth0_jwks(): """Get Auth0 JWKs. Returns: object: auth0 jwks """ auth0_jwks = json.loads(urlopen( '{}.well-known/jwks.json'.format( config.AUTH_ISSUERS_LIST[0])).read().decode()) return auth0_jwks def extract_jwt_token(header): """Extract the jwt token from a header. Args: header (str): authorization header to get the value from. Returns: str: the jwt token (if found) """ if not header: return None header = header.strip().split(' ') if len(header) != 2 or header[0] != 'Bearer': return None return header[1] def validate_and_decode_jwt_token( token: str, jwt_auth_client: Optional[JWTAuth] = None, ) -> response.Response: """Validate jwt token with JWTAuth.""" if not jwt_auth_client: jwt_auth_client = jwt_auth_from_environment(config.ENVIRONMENT) try: decoded_token = jwt_auth_client.get_token(token) except JWTAuthError as e: return response.create_error_response( code=errors.UNAUTHORIZED_CODE, message=e.message, status=response_status.UNAUTHORIZED) return response.Response(decoded_token) def cache_jwks(jwks): """Cache jwks. Returns: None """ cache_obj = get_uwsgi_cache_object() cache_obj.set('jwks', jwks, 0) def cache_jwt_token(): """Cache jwt token. Returns: None """ warn( 'Shared M2M JWT is being deprecated. See PP-546, PP-547.', PendingDeprecationWarning, stacklevel=2) cache_expiration = {} secrets_manager_client = get_secrets_manager_client( auth_constants.JWT_SERVICE_NAME) jwt_token = get_jwt(secrets_manager_client) token_expiration_time = jwt_token.get( auth_constants.JWT_EXPIRY_SECRET_KEY, None) if token_expiration_time is not None: cache_expiration = get_jwt_cache_expiry(token_expiration_time) expiration_in_secs = cache_expiration.get('jwt_expiry_secs', -1) cache_obj = get_uwsgi_cache_object() cache_obj.set('jwt_token', jwt_token.get( auth_constants.JWT_SECRET_KEY, None), expiration_in_secs) cache_obj.set( 'jwt_expiration_time', cache_expiration.get( 'jwt_expiration_time', None), expiration_in_secs) @retry(stop_max_attempt_number=auth_constants.JWT_SECRETS_RETRY_COUNT) def get_secret(secrets_manager_client, secret_name): """Get secret from aws secrets manager. Args: secrets_manager_client (object): PythonSecretsManager client secret_name (str): Name of the secret Returns: str: secret value """ return secrets_manager_client.get_cred(secret_name) def get_jwt(secrets_manager_client): """Generate jwt and jwt expiration. Args: secrets_manager_client (object): PythonSecretsManager client Returns: dict: jwt and token expiration time """ warn( 'Shared M2M JWT is being deprecated. See PP-546, PP-547.', PendingDeprecationWarning, stacklevel=2) try: return { auth_constants.JWT_SECRET_KEY: get_secret( secrets_manager_client, auth_constants.JWT_SECRET_KEY), auth_constants.JWT_EXPIRY_SECRET_KEY: get_secret( secrets_manager_client, auth_constants.JWT_EXPIRY_SECRET_KEY) } except Exception as e: logger.info( f'Failed to fetch jwt secrets for ' f'{config.ENVIRONMENT} Exception: {str(e)}') return {} def get_jwt_cache_expiry(jwt_token_expiration): """Compute jwt cache expiry. Args: jwt_token_expiration (str): jwt token expiration utc time. Returns: dict: cache expiration time and expiry in seconds. """ warn( 'Shared M2M JWT is being deprecated. See PP-546, PP-547.', PendingDeprecationWarning, stacklevel=2) try: # Current Time current_time = datetime.utcnow() # JWT expiration Time jwt_expiration_date_time_obj = datetime.strptime( jwt_token_expiration, '%Y-%m-%d %H:%M:%S.%f') # Random jitter random_jitter = random.randint(0, 60) # Actual expiration time for cache with delta and jitter in seconds actual_expiration_time = \ jwt_expiration_date_time_obj - timedelta( seconds=auth_constants.TOKEN_EXPIRATION_DELTA - random_jitter) # JWT time to expiry datetime object token_expiration = actual_expiration_time - current_time # JWT time to expiry in seconds token_expiration_in_seconds = token_expiration.days * 24 * 3600 + \ token_expiration.seconds return { 'jwt_expiration_time': actual_expiration_time, 'jwt_expiry_secs': token_expiration_in_seconds } except Exception as e: logger.info( f'Failed to compute jwt cache timeout in ' f'{config.ENVIRONMENT} Exception: {str(e)}') # If cache timeout not computed as expected, default timeout is set return { 'jwt_expiration_time': None, 'jwt_expiry_secs': -1 } def is_jwt_cached(sender, receiver): """Check if jwt is present in cache. Args: sender (string): sender service of request receiver (string): receiver service of request Returns: bool: True if jwt exists in cache. """ warn( 'Shared M2M JWT is being deprecated. See PP-546, PP-547.', PendingDeprecationWarning, stacklevel=2) cache_obj = get_uwsgi_cache_object() jwt_is_cached = False if ( cache_obj.has('jwt_enabled_services') and receiver in cache_obj.get('jwt_enabled_services') ): jwt_is_cached = ( cache_obj.has('jwt_token') and cache_obj.get('jwt_token') is not None ) if not jwt_is_cached: logger.info( f'JWT is not available in cache while sending request from ' f'{sender} to {receiver}. ') return jwt_is_cached def is_jwt_enabled_service(service): """Check if jwt enabled service. Args: service (string): service name Returns: bool: True if jwt enabled service. """ cache_obj = get_uwsgi_cache_object() return cache_obj.has('jwt_enabled_services') and \ service in cache_obj.get('jwt_enabled_services') def initialize_cache(sender): """Run threads to cache jwt, jwt services in uwsgi with periodic refresh. Args: sender (str): Service that sends request. Returns: None """ uwsgi_cache_refresh_threads.ThreadingAsynch(sender) def get_secrets_manager_client(service): """Get secrets_manager object for the service. Args: service (str): Service name. Returns: object """ return PythonSecretsManager( application_context=False, environment=config.ENVIRONMENT, service_name=service) def get_jwt_enabled_services(secrets_manager_client): """Generate jwt enabled services. Args: secrets_manager_client (obj): secrets_manager_client instance. Returns: list: jwt enabled services list """ try: return get_secret( secrets_manager_client, auth_constants.JWT_ENABLED_SERVICES_SECRET_KEY) except Exception as e: logger.info( f'Failed to fetch jwt enabled services from secret for ' f'{config.ENVIRONMENT} Exception: {str(e)}') return None def cache_jwt_enabled_services(sender=None): """Cache jwt enabled services list. Args: sender (str): Sender service of request. Returns: None """ secrets_manager_client = get_secrets_manager_client( auth_constants.JWT_SERVICE_NAME) jwt_enabled_list = [] jwt_enabled_services = get_jwt_enabled_services(secrets_manager_client) if jwt_enabled_services is not None and \ isinstance(jwt_enabled_services, list): try: if sender and sender not in jwt_enabled_services: jwt_enabled_services.append(sender) update_secret( secrets_manager_client, auth_constants.JWT_ENABLED_SERVICES_SECRET_KEY, json.dumps(jwt_enabled_services)) jwt_enabled_list = jwt_enabled_services except Exception as e: logger.info( f'Failed to update jwt enabled services secret value for ' f'{config.ENVIRONMENT} Exception: {str(e)}') if jwt_enabled_list: cache_obj = get_uwsgi_cache_object() cache_obj.set('jwt_enabled_services', jwt_enabled_list, 0) @retry(stop_max_attempt_number=auth_constants.JWT_SECRETS_RETRY_COUNT) def update_secret(secrets_manager_client, secret_name, value): """Update value of secret in aws secrets manager. Args: secrets_manager_client (object): PythonSecretsManager client secret_name (str): Name of the secret value (string): Value to be save in secret Returns: str: secret value """ return secrets_manager_client.update_secret(secret_name, value) def get_uwsgi_cache_object(): """Get uwsgi cache object. Returns: object: uwsgi cache object """ return UWSGICache(cache='uwsgi_cache')