Work on API keys and tokens

This commit is contained in:
Scott Idem
2021-07-14 17:12:20 -04:00
parent 6f8e18750c
commit 6bb2d7f761
4 changed files with 212 additions and 6 deletions

View File

@@ -12,3 +12,4 @@ pytz
stripe stripe
passlib passlib
argon2_cffi argon2_cffi
PyJWT

View File

@@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
import datetime, pytz, redis import datetime, jwt, pytz, redis, time
from passlib.hash import argon2 from passlib.hash import argon2
#from datetime import datetime, time, timedelta #from datetime import datetime, time, timedelta
@@ -45,8 +45,6 @@ async def get_account_header(x_account_id:str = Header(...)):
# ### END ### API Lib General ### async get_account_header() ### # ### END ### API Lib General ### async get_account_header() ###
def secure_hash_string(string:str): def secure_hash_string(string:str):
string_hash = argon2.using(rounds=14, memory_cost=1536, parallelism=2).hash(string) string_hash = argon2.using(rounds=14, memory_cost=1536, parallelism=2).hash(string)
@@ -57,4 +55,55 @@ def verify_secure_hash_string(string:str, string_hash:str):
if argon2.verify(string, string_hash): if argon2.verify(string, string_hash):
return True return True
else: else:
return False return False
# Updated 2021-07-14
def sign_jwt(
secret_key: str, # Secret/Private/Password
public_key: str, # Will be part of the token. Use to look up secret when verifying.
ttl: int = 60, # Default to 60 seconds
max_renew: int = 0, # Default to 0
account_id: str = None,
person_id: str = None,
user_id: str = None,
) -> Dict[str, str]:
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
payload = {
'iat': time.time(), # Issued at
'eat': time.time() + ttl, # Expires at
'max_renew': max_renew, # Number of times allowed to request renew without API secret key
'public_key': public_key, # Use to lookup the secret/private/password key when verifying
'account_id': account_id,
'person_id': person_id,
'user_id': user_id,
}
secret = secret_key
algorithm = 'HS256'
token = jwt.encode(payload, secret, algorithm=algorithm)
log.debug(token)
return token
# Updated 2021-07-14
def decode_jwt(
secret_key: str,
token: str,
) -> dict:
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
secret = secret_key
algorithm = 'HS256'
try:
decoded_token = jwt.decode(token, secret, algorithms=[algorithm])
log.debug(decoded_token)
if decoded_token['eat'] >= time.time(): return decoded_token
else: return False
except:
return None

View File

@@ -274,6 +274,7 @@ origins = [
'http://localhost:7800', 'http://localhost:7800',
'https://oneskyit.com', 'https://oneskyit.com',
'http://dev-idaa.localhost:5000', 'http://dev-idaa.localhost:5000',
'http://dev.home:5000',
'http://connect.home:5000', 'http://connect.home:5000',
'http://connect.localhost:5000', 'http://connect.localhost:5000',
'http://svelte.localhost:5555', 'http://svelte.localhost:5555',

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, stat
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union from typing import Dict, List, Optional, Set, Union
from app.lib_general import log, logging from app.lib_general import log, logging, sign_jwt, decode_jwt
from app.config import settings from app.config import settings
from app.db_sql import sql_insert, sql_update, sql_insert_or_update, sql_select, sql_delete, redis_lookup_id_random from app.db_sql import sql_insert, sql_update, sql_insert_or_update, sql_select, sql_delete, redis_lookup_id_random
@@ -17,6 +17,161 @@ from app.models.response_models import Resp_Body_Base, mk_resp
router = APIRouter() router = APIRouter()
# Generate JWT using associated API private key
# Verify JWT using the API public key's associated API private key
# API server or trusted app can generate JWTs
# JWT contains:
# * client_token (to request a new short term client token)
# * iat
# * eat
# * account_id
# * client_id
# * order_cart_id
# * person_id
# * user_id
# API server verifies JWTs
@router.get('/request_jwt', response_model=Resp_Body_Base)
async def request_jwt(
x_aether_api_secret_key: Optional[str] = Header(None, min_length=22, max_length=22), # If passed then can also set TTL
x_aether_api_public_key: Optional[str] = Header(None, min_length=22, max_length=22), # Used to look up the API secret if not given
x_aether_api_token: Optional[str] = Header(None), # Token given to client by an API key holder (short max TTL)
account_id: str = None,
session_id: str = None, # End client (web browser)
client_id: str = None, # End client (web browser)
person_id: str = None,
user_id: str = None,
max_ttl: int = 300, # Number of seconds to live. Only use if given the API secret key.
# Seconds: 3600 = 1 hr; 300 = 5 min
max_renew: int = 5, # Decrease count by 1 until 0 if only sent a current API token.
):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
if x_aether_api_secret_key or x_aether_api_token: pass
else: return mk_resp(data=False, status_code=400) # Bad Request
api_secret_key = x_aether_api_secret_key
if x_aether_api_secret_key:
log.debug(f'Contains a value in x_aether_api_secret_key: {x_aether_api_secret_key}')
table_name_select = 'api_key'
field_name = 'secret_key'
field_value = api_secret_key
if api_key_rec_select_result := sql_select(table_name=table_name_select, field_name=field_name, field_value=field_value): pass
else:
log.warning('No results when looking up the API secret key')
return mk_resp(data=False, status_code=401) # Unauthorized
# if api_key_rec_select_result.get('enable', None):
# api_key_rec = api_key_rec_select_result
# else:
# log.warning('API secret key not enabled')
# return mk_resp(data=False, status_code=401) # Unauthorized
# current_datetime = datetime.datetime.utcnow() # datetime.datetime.now() Gets server local datetime
# if api_key_rec.get('enable_from', None) <= current_datetime and api_key_rec.get('enable_to', None) >= current_datetime:
# pass
# else:
# log.warning('API secret key expired')
# return mk_resp(data=False, status_code=401) # Unauthorized
# if api_public_key := api_key_rec.get('public_key', None): pass
# else:
# log.warning('Public key was not found with the API secret key that was looked up')
# return mk_resp(data=False, status_code=400) # Bad Request
# max_ttl = 3600
elif x_aether_api_public_key and x_aether_api_token:
table_name_select = 'api_key'
field_name = 'public_key'
field_value = x_aether_api_public_key
if api_key_rec_select_result := sql_select(table_name=table_name_select, field_name=field_name, field_value=field_value): pass
else:
log.warning('No results when looking up the API public key')
return mk_resp(data=False, status_code=401) # Unauthorized
# Check if the API keys are valid
if api_key_rec_select_result.get('enable', None):
api_key_rec = api_key_rec_select_result
else:
log.warning('API secret key not enabled')
return mk_resp(data=False, status_code=401) # Unauthorized
current_datetime = datetime.datetime.utcnow() # datetime.datetime.now() Gets server local datetime
if api_key_rec.get('enable_from', None) <= current_datetime and api_key_rec.get('enable_to', None) >= current_datetime:
pass
else:
log.warning('API secret key expired')
return mk_resp(data=False, status_code=401) # Unauthorized
if api_secret_key := api_key_rec.get('secret_key', None): pass
else:
log.warning('Secret key was not found')
return mk_resp(data=False, status_code=400) # Bad Request
if api_public_key := api_key_rec.get('public_key', None): pass
else:
log.warning('Public key was not found')
return mk_resp(data=False, status_code=400) # Bad Request
# Decode the JWT if an API token was sent and the API secret key was sent/found.
if x_aether_api_token and api_public_key and api_secret_key:
if current_token := decode_jwt(secret_key=api_secret_key, token=x_aether_api_token):
if current_token.get('max_renew', 0) > 0: pass
else:
message = 'The JWT sent is out of allowed renewals. Try again with a current JWT or just the API secret key.'
log.warning(message)
return mk_resp(data=False, status_code=401, status_message=message) # Unauthorized
max_ttl = 300
max_renew = current_token.get('max_renew', 0) - 1
if not account_id: account_id = current_token.get('account_id', None)
if not person_id: person_id = current_token.get('person_id', None)
if not user_id: user_id = current_token.get('user_id', None)
else:
message = 'The JWT sent is either expired or otherwise invalid. Try again with a current JWT or just the API secret key.'
log.warning(message)
return mk_resp(data=False, status_code=401, status_message=message) # Unauthorized
# api_key_rec = api_key_rec_select_result
# api_secret_key = x_aether_api_secret_key
# if api_key_rec_select_result.get('enable', None):
# api_key_rec = api_key_rec_select_result
# else:
# log.warning('API secret key not enabled')
# return mk_resp(data=False, status_code=401) # Unauthorized
# if x_aether_api_token:
# if current_token := decode_jwt(secret_key=api_secret_key, token=x_aether_api_token):
# if current_token.get('count', 0) > 0: pass
# else:
# message = 'The JWT sent is out of allowed renewals. Try again with a current JWT or just the API secret key.'
# log.warning(message)
# return mk_resp(data=False, status_code=401, status_message=message) # Unauthorized
# max_ttl = 300
# max_renew = current_token.get('max_renew', 0) - 1
# if not account_id: account_id = current_token.get('account_id', None)
# if not person_id: person_id = current_token.get('person_id', None)
# if not user_id: user_id = current_token.get('user_id', None)
# else:
# message = 'The JWT sent is either expired or otherwise invalid. Try again with a current JWT or just the API secret key.'
# log.warning(message)
# return mk_resp(data=False, status_code=401, status_message=message) # Unauthorized
payload = {}
payload['account_id'] = account_id
payload['person_id'] = person_id
payload['user_id'] = user_id
token = sign_jwt(secret_key=api_secret_key, public_key=api_public_key, ttl=max_ttl, max_renew=max_renew, **payload)
response_data = { 'api_access_jwt': token }
return mk_resp(data=response_data)
@router.get('/temp_token', response_model=Resp_Body_Base) @router.get('/temp_token', response_model=Resp_Body_Base)
async def get_api_temp_token( async def get_api_temp_token(
x_aether_api_key: Optional[str] = Header(None), x_aether_api_key: Optional[str] = Header(None),
@@ -34,7 +189,7 @@ async def get_api_temp_token(
log.debug(f'Contains a value in x_aether_api_key: {x_aether_api_key}') log.debug(f'Contains a value in x_aether_api_key: {x_aether_api_key}')
sql_result = sql_select(table_name=table_name_select, field_name=field_name, field_value=field_value) sql_result = sql_select(table_name=table_name_select, field_name=field_name, field_value=field_value)
else: else:
return mk_resp(data=False, status_code=400) return mk_resp(data=False, status_code=400) # Bad Request
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
if sql_result: if sql_result: