Package: apojwt
ApoJWTTheapojwtPackage was created with the intention of providing JWT support to Intent's Apogee Microservices. These services require a hierarchy of permissions that vary across all endpoints. As such, this package aims to provide decorators that can be attached with route declarations to ensure a valid JWT with proper permissions is being sent in the request headers. The package is intended to be used alongside a Python API framework such as Flask or FastAPI.ApoJWT ClassThe ApoJWT class has the following constructor:(self,secret:str,exp_period:int=900,iss:str="",server_audience:list=[],algorithm:str="HS256",template=None,async_framework:bool=False,token_finder=None,exception_handler=None)"""Keyword Arguments (those with asterisks are functions):JWT Validationsecret:Secret string used to encode and decode the access JWTexp_period:Length of time in seconds access tokens is valid for.Default 900 (15 minutes)iss:Issuer string used for additional security.Default ""server_audience:Audience names of expected HTTP hosts.Audience names are typically base address URLs.Ex: https://example.comalgorithm:The algorithm to use when encoding/decoding.Default HS256admin_permission:Optional full admin permissionJWTs carrying this will always be authorizedFramework Configurationasync_framework:If True - ApoJWT awaits the decorated function(FastAPI needs this True)Default: Falsetoken_finder:Returns the Access JWT (framework specific)Typically found as the "Authorization" headerDefault: NoneExpected Function Structure:(*args, **kwargs) -> strexception_handler:HTTP error handling function (framework specific)Expected Function Structure:(code: int, msg: str) -> None"""Higher Order Functionality in ApoJWTToken FinderThe token_finder function must be passed to the higher order constructor (if a template is not given) for decorated token validation to succeed. The function must return the JWT string, which can usually be found in the HTTP request headers with the key 'Authorization'. It is standard for JWTs to be prefixed with the word 'Bearer'. It will be up to this function to remove this substring.Expected Function Structure:(*args, **kwargs) -> strNOTE:argsandkwargsare the same arguments given to the HTTP request handler and could be optionalExample# Flask's request objectrequest.headers["Authorization"]>>>'Bearer <token>'request.headers["Authorization"].replace("Bearer ","")>>>'<token>'"""Token Finder: used to locate and return the JWT"""# FastAPItoken_finder=lambda**kwargs:kwargs["Authorization"].replace("Bearer ","")ajwt=("secret",iss="issuer",async_framework=True,token_finder=token_finder)## NOTE: async_framework is True for FastAPI# Flasktoken_finder=lambda:request.headers["Authorization"].replace("Bearer ","")ajwt=("secret",iss="issuer",token_finder=token_finder)## NOTE: async_framework defaults to False for FlaskException HandlerThe exception handler is optional, but allows for decorated validation to properly be handled with an HTTP error response provided by the HTTP framework in use.Expected Function Structure:(code: int, msg: str, *args, **kwargs) -> NoneExample"""Exception Handler"""# FastAPIdefexception_handler(code:int,msg:str,*args,**kwargs):raiseHTTPException(status_code=code,detail=msg)ajwt=("secret",iss="issuer",async_framework=True,token_finder=...,exception_handler=exception_handler)# Flaskdefexception_handler(code:int,msg:str):abort(code,msg)ajwt=("secret",iss="issuer",token_finder=...,exception_handler=exception_handler)DecoratorsDecorators are the main use case of the ApoJWT package after initialization. They allow any endpoint to be secured with a single simple line of code.ajwt=ApoJWT(secret,iss,token_finder=lambda:...,...)@ajwt.token_required"""Validates JWTCan return 'token_data' and 'token_subject' as kwargs to HTTP handler"""@ajwt.permission_required(permission_name:str)"""Validates JWT and ensures permission_name is among the token permissionspermission_name: a permission stringCan return 'token_data' and 'token_subject' as kwargs to HTTP handler"""Both decorators returntoken_dataandtoken_subjectas keyword arguments to the HTTP handler that is being decorated. With these arguments, the additional data stored in the JWT and the JWT's subject are both accessible.Example# fast api@app.get("/some/endpoint")@ajwt.token_requireddefsome_endpoint(authorization=Header(None),# requiredtoken_data:Optional[dict]=Body(None),# optionaltoken_subject:Optional[str]=Body(None)# optional):...# flask@app.route("/some/endpoint",methods=["GET"])@ajwt.permission_required("permission")defsome_endpoint(token_data:dict,# optionaltoken_subject:str# optional):...Functionsajwt=ApoJWT(...)ajwt.create_token(self,sub:str="",permissions:list[str]=[],data:dict=dict(),refresh_data:dict=dict()):"""Encodes access and refresh* JWT(s)*if configuredsub:Subject of the JWT(typically a reference to the user of JWT)permissions:List of permissions to assign to tokendata:Any additional data that is neededrefresh_data:IF refresh is configured:Additional data stored with the refresh tokenJWT will contain the following claims:- exp: Expiration Time- nbf: Not Before Time- iss: Issuer- aud: Audience- iat: Issued At"""Refresh TokensApoJWT 1.5.0 introduced Refresh Token functionality. This feature is highly recommended to provide an extra layer of security to applications. To read up on Refresh Tokens and their benefits, check outthis Auth0 articlefor more information. The Refresh functionality in ApoJWT is activated with the following function:ajwt.config_refresh(refresh_secret:str,refresh_exp_period:int=86400,refresh_finder=None):"""Configures ApoJWT for use with refresh tokensrefresh_secret:Secret used to encode and decode the refresh JWTrefresh_exp_period:Number of seconds refresh JWT is validDefault 86400 (1 day)refresh_finder:Function to retrieve the refresh JWTDefault None"""The functionrefresh_finderis a similar function totoken_finderin that it must return the refresh token. The main difference is thatrefresh_finder, in most cases, should find the refresh token in an http-only secure cookie instead of the HTTP Authorization header.Expectedrefresh_finderFunction Structure:(*args, **kwargs) -> strOnce this function is called and initialized, ApoJWT is equipped to handle Refresh Tokens.Refresh FunctionalityThecreate_tokenfunction will now return a tuple containing the access token and the refresh tokenaccess,refresh=ajwt.create_token(...)Typically, this refresh token can then be stored in an HTTP-only cookie.From there, the@ajwt.refreshdecorator can be placed on any endpoint where a refresh should occur. This will return the refresh_data stored in the token. This can be another reference to the user which could be used to reauthorize.Example# Here, the refresh data stores a user_id# Fast Api@app.get("/some/endpoint")@ajwt.refreshdefrefresh(refresh_data:dict):user_id=refresh_data["user_id"]user_permissions=get_user_permissions(user_id)ref_data=dict(user_id=user_id)access_token,refresh_token=ajwt.create_token(sub=user_id,permissions=user_permissions,refresh_data=ref_data)Usage ExamplesConstructing ApoJWT# FastAPIajwt=("secret",iss="issuer",template="fastapi"# configures ApoJWT for fastapi)# Flaskajwt=("secret",iss="issuer",template="fastapi"# configures ApoJWT for flask)Validating JWT with Decorators# fast api@app.get("/some/endpoint")@ajwt.permission_required("permission")defsome_endpoint(authorization=Header(None)):...# flask@app.route("/some/endpoint",methods=["GET"])@ajwt.token_requireddefsome_endpoint()...Refresh Configuration# flaskajwt.config_refresh("refresh_secret",refresh_finder=lambda:request.cookies.get('refresh_token'))# fast apidefrefresh_finder(refresh_token:Union[str,None]=Cookie(default=None)):returnrefresh_tokenajwt.config_refresh("refresh_secret",refresh_finder=refresh_finder)Creating a New JWT"""Permissions will be assigned to the new token"""sub="user_id_1"permissions=["permission",...]data=dict(...=...)# If refresh IS NOT configured# NOTE: all arguments are optionaltoken=ajwt.create_token(sub=sub,permissions=permissions,data=data)# If refresh IS configuredrefresh_data=dict(...=...)access,refresh=ajwt.create_token(sub=sub,permissions=permissions,data=data,refresh_data=refresh_data)Getting Token Data and Subject from JWT# flask@app.route("/...")@ajwt.token_requireddefroute(token_data:dict,token_subject:str):print(token_subject)returntoken_data# fastapi@app.get("/...")@ajwt.permission_required("...")defroute(authorization=Header(None),# token_data and token_subject are unexpected to fastapi# Optional forces fastapi to ignore unexpected parameterstoken_data:Optional[dict]=Body(None),token_subject:Optional[str]=Body(None))