OpenAPI provides support for basic HTTP authentication through security, but different web frameworks implement basic HTTP authentication in different ways.
So Pait provides simple support for OpenAPI's security through Depends (api key, http, oauth2), which simplifies the use of security in different web frameworks.
Note
advanced authentication such as jwt will be supported through other package in the future.
1.APIKey
API Key is the simplest method in Security, and because of its simplicity, it has the most usage scenarios.
Pait provides APIKey class to support the use of API Key, the usage is as follows:
The first highlighting code is initialized for the different APIKey fields,
which use slightly different parameters, see the table below for parameter meanings:
Parameters
Description
name
The name of the APIKey field
field
The APIKey field corresponds to the Field class in Pait, API Key only supports Query, Header and Cookie parameters, so only field.Query, field.Header, field.Cookie are allowed
verify_api_key_callable
A function that checks APIKey, Pait extracts the APIKey value from the request body and passes it to the function for processing, if the function returns True then the check passes, and vice versa then the check fails.
security_name
Specify the name of the security, the name of APIKey must be different for different roles, the default value is the class name of APIKey.
Note
In order to use APIKey properly in the OpenAPI tool, the Field must be initialized with the openapi_include attribute False.
The second highlighted piece of code connects APIKey to the route function via Depend,
where the argument to Depend is an instance of APIKey.
When the route function receives a request Pait automatically extracts the value of APIKey from the request body and passes it to APIKey's verify_api_key_callable function for verification,
if it passes the verification the value is injected into the route function for execution,
and vice versa 401 is returned.
After running the code, execute the following command to see the execution effect of APIKey:
Most of the parameters (e.g., Token) needed by route functions that use APIKey are obtained through other route functions.
In this case, you can describe the relationship between this route function and other route functions by using Links in Field,
such as the following scenario, which gets its Token from the login route function:
importhashlibfromtypingimportTypefrompydanticimportBaseModel,Fieldfromtornado.ioloopimportIOLoopfromtornado.webimportApplication,RequestHandlerfrompait.app.tornadoimportpaitfrompait.app.tornado.securityimportapi_keyfrompait.fieldimportDepends,Json,Queryfrompait.model.responseimportJsonResponseModelfrompait.openapi.doc_routeimportAddDocRoutefrompait.openapi.openapiimportLinksModelclassLoginRespModel(JsonResponseModel):classResponseModel(BaseModel):classDataModel(BaseModel):token:strcode:int=Field(0,description="api code")msg:str=Field("success",description="api status msg")data:DataModeldescription:str="login response"response_data:Type[BaseModel]=ResponseModelclassLoginHandler(RequestHandler):@pait(response_model_list=[LoginRespModel])asyncdefpost(self,uid:str=Json.i(description="user id"),password:str=Json.i(description="password"))->None:self.write({"code":0,"msg":"","data":{"token":hashlib.sha256((uid+password).encode("utf-8")).hexdigest()}})link_login_token_model:LinksModel=LinksModel(LoginRespModel,"$response.body#/data/token",desc="test links model")token_query_api_key:api_key.APIKey=api_key.APIKey(name="token",field=Query(links=link_login_token_model),verify_api_key_callable=lambdax:"token"inx,security_name="token-query-api-key",)classAPIKeyQueryHandler(RequestHandler):@pait()asyncdefget(self,token:str=Depends.i(token_query_api_key))->None:self.write({"code":0,"msg":"","data":token})app:Application=Application([(r"/api/login",LoginHandler),(r"/api/security/api-query-key",APIKeyQueryHandler),],)AddDocRoute(app)if__name__=="__main__":app.listen(8000)IOLoop.instance().start()
The first highlighted code is from Field-Links, while the Query in the second highlighted code sets the links attribute to link_login_token_model.
This way Pait will bind login_route to api_key_query_route via Link when generating OpenAPI.
Note
openapi_include=False causes Swggaer to be unable to display Link data.
2.HTTP
There are two types of HTTP basic authentication, one is HTTPBasic and the other is HTTPBearer or HTTPDIgest.
The difference between the two is that HTTPBasic needs to verify the username and password parameters in the request header,
and if the verification is successful, it means the authentication is successful.
If the validation error will return 401 response, the browser will pop up a window to let the user enter username and password.
While HTTPBearer or HTTPDIgest only need to pass token in the request header as required.
Pait encapsulates the HTTPBasic, HTTPBearer and HTTPDigest classes for each of the three methods of HTTP basic authentication.
Like APIKey they need to be bound to a route function via Depend, which is used as follows:
You can see that the whole code consists of two parts, the first part initializes the corresponding basic authentication class,
and the second part uses Depend in the route function to get an instance of the authentication class.
However, HTTPBasic is used a little differently than the other two,
for example, HTTPBasic has different initialization parameters than the other two, which are described in the following table:
parameter
description
security_model
OpenAPI Description Model about HTTPBasic, a generic HTTPBasicModel has been provided by default, for customization needs visit OpenAPI's securitySchemeObject
security_name
Specifies the name of the Security, the name of the basic authentication instance must be different for different roles, the default value is class name.
header_field
Instance of Header Field for Pait
realm
The realm parameter of HTTP basic authentication
While HTTPBearer and HTTPDigest are used in a similar way as APIKey,
they need to be initialized as required and bound to the route function via Depend,
their parameters are described as follows:
parameter
description
security_model
OpenAPI Description Model about HTTPBasic, a generic HTTPBasicModel has been provided by default, for customization needs visit OpenAPI's securitySchemeObject
security_name
Specifies the name of the Security, the name of the basic authentication instance must be different for different roles, the default value is class name.
header_field
Instance of Header Field for Pait
is_raise
When set to True, Pait throws a standard error if parsing fails, and False returns None if parsing fails, default True.
verify_callable
Accepts a checksum function, Pait extracts the value from the request body and passes it to the checksum function, if it returns True it means the checksum passes, otherwise the checksum fails.
In addition to the difference in initialization parameters,
HTTPBasic is not used directly in the route function,
but exists in the get_user_name function and the get_user_name function is responsible for authentication.
returning the username to the route function if the authentication is successful, otherwise returning a 401 response.
After running the code, execute the curl command to see them executed as follows:
The HTTPDigest class only provides simple HTTPDigest authentication support,
which needs to be modified to suit your business logic when using it.
3.Oauth2
OAuth 2.0 is an authorization protocol that provides API clients with limited access to user data on the Web server.
In addition to providing identity verification, it also supports privilege verification, which is used as follows:
importrandomimportstringfromtypingimportTYPE_CHECKING,Callable,Dict,List,OptionalfromflaskimportFlaskfrompydanticimportBaseModel,Fieldfromwerkzeug.exceptionsimportBadRequestfrompait.app.flaskimportpaitfrompait.app.flask.securityimportoauth2frompait.fieldimportDependsfrompait.model.responseimportHttp400RespModelfrompait.openapi.doc_routeimportAddDocRouteifTYPE_CHECKING:frompait.app.base.security.oauth2importBaseOAuth2PasswordBearerProxyclassUser(BaseModel):uid:str=Field(...,description="user id")name:str=Field(...,description="user name")age:int=Field(...,description="user age")sex:str=Field(...,description="user sex")scopes:List[str]=Field(...,description="user scopes")temp_token_dict:Dict[str,User]={}@pait(response_model_list=[Http400RespModel,oauth2.OAuth2PasswordBearerJsonRespModel],)defoauth2_login(form_data:oauth2.OAuth2PasswordRequestFrom)->dict:ifform_data.username!=form_data.password:raiseBadRequest()token:str="".join(random.choice(string.ascii_letters+string.digits)for_inrange(10))temp_token_dict[token]=User(uid="123",name=form_data.username,age=23,sex="M",scopes=form_data.scope)returnoauth2.OAuth2PasswordBearerJsonRespModel.response_data(access_token=token).dict()oauth2_pb:oauth2.OAuth2PasswordBearer=oauth2.OAuth2PasswordBearer(route=oauth2_login,scopes={"user-info":"get all user info","user-name":"only get user name",},)defget_current_user(_oauth2_pb:"BaseOAuth2PasswordBearerProxy")->Callable[[str],User]:def_check_scope(token:str=Depends.i(_oauth2_pb))->User:user_model:Optional[User]=temp_token_dict.get(token,None)ifnotuser_model:raise_oauth2_pb.security.not_authenticated_excifnot_oauth2_pb.is_allow(user_model.scopes):raise_oauth2_pb.security.not_authenticated_excreturnuser_modelreturn_check_scope@pait()defoauth2_user_name(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-name"]))))->dict:return{"code":0,"msg":"","data":user_model.name}@pait()defoauth2_user_info(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-info"]))))->dict:return{"code":0,"msg":"","data":user_model.dict()}app=Flask("demo")app.add_url_rule("/api/oauth2-login",view_func=oauth2_login,methods=["POST"])app.add_url_rule("/api/oauth2-user-name",view_func=oauth2_user_name,methods=["GET"])app.add_url_rule("/api/oauth2-user-info",view_func=oauth2_user_info,methods=["GET"])AddDocRoute(app)if__name__=="__main__":app.run(port=8000)
importrandomimportstringfromtypingimportTYPE_CHECKING,Callable,Dict,List,OptionalfrompydanticimportBaseModel,Fieldfromstarlette.applicationsimportStarlettefromstarlette.exceptionsimportHTTPExceptionfromstarlette.responsesimportJSONResponsefrompait.app.starletteimportpaitfrompait.app.starlette.securityimportoauth2frompait.fieldimportDependsfrompait.model.responseimportHttp400RespModel,TextResponseModelifTYPE_CHECKING:frompait.app.base.security.oauth2importBaseOAuth2PasswordBearerProxyclassUser(BaseModel):uid:str=Field(...,description="user id")name:str=Field(...,description="user name")age:int=Field(...,description="user age")sex:str=Field(...,description="user sex")scopes:List[str]=Field(...,description="user scopes")temp_token_dict:Dict[str,User]={}@pait(response_model_list=[oauth2.OAuth2PasswordBearerJsonRespModel,Http400RespModel.clone(resp_model=TextResponseModel),],)asyncdefoauth2_login(form_data:oauth2.OAuth2PasswordRequestFrom)->JSONResponse:ifform_data.username!=form_data.password:raiseHTTPException(400)token:str="".join(random.choice(string.ascii_letters+string.digits)for_inrange(10))temp_token_dict[token]=User(uid="123",name=form_data.username,age=23,sex="M",scopes=form_data.scope)returnJSONResponse(oauth2.OAuth2PasswordBearerJsonRespModel.response_data(access_token=token).dict())oauth2_pb:oauth2.OAuth2PasswordBearer=oauth2.OAuth2PasswordBearer(route=oauth2_login,scopes={"user-info":"get all user info","user-name":"only get user name",},)defget_current_user(_oauth2_pb:"BaseOAuth2PasswordBearerProxy")->Callable[[str],User]:def_check_scope(token:str=Depends.i(_oauth2_pb))->User:user_model:Optional[User]=temp_token_dict.get(token,None)ifnotuser_model:raise_oauth2_pb.security.not_authenticated_excifnot_oauth2_pb.is_allow(user_model.scopes):raise_oauth2_pb.security.not_authenticated_excreturnuser_modelreturn_check_scope@pait()defoauth2_user_name(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-name"]))),)->JSONResponse:returnJSONResponse({"code":0,"msg":"","data":user_model.name})@pait()defoauth2_user_info(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-info"]))),)->JSONResponse:returnJSONResponse({"code":0,"msg":"","data":user_model.dict()})app=Starlette()app.add_route("/api/oauth2-login",oauth2_login,methods=["POST"])app.add_route("/api/oauth2-user-name",oauth2_user_name,methods=["GET"])app.add_route("/api/oauth2-user-info",oauth2_user_info,methods=["GET"])if__name__=="__main__":importuvicornuvicorn.run(app)
importrandomimportstringfromtypingimportTYPE_CHECKING,Callable,Dict,List,OptionalfrompydanticimportBaseModel,FieldfromsanicimportHTTPResponse,Sanic,jsonfromsanic.exceptionsimportInvalidUsagefrompait.app.sanicimportpaitfrompait.app.sanic.securityimportoauth2frompait.fieldimportDependsfrompait.model.responseimportHttp400RespModelfrompait.openapi.doc_routeimportAddDocRouteifTYPE_CHECKING:frompait.app.base.security.oauth2importBaseOAuth2PasswordBearerProxyclassUser(BaseModel):uid:str=Field(...,description="user id")name:str=Field(...,description="user name")age:int=Field(...,description="user age")sex:str=Field(...,description="user sex")scopes:List[str]=Field(...,description="user scopes")temp_token_dict:Dict[str,User]={}@pait(response_model_list=[Http400RespModel,oauth2.OAuth2PasswordBearerJsonRespModel],)asyncdefoauth2_login(form_data:oauth2.OAuth2PasswordRequestFrom)->HTTPResponse:ifform_data.username!=form_data.password:raiseInvalidUsage("Bad Request")token:str="".join(random.choice(string.ascii_letters+string.digits)for_inrange(10))temp_token_dict[token]=User(uid="123",name=form_data.username,age=23,sex="M",scopes=form_data.scope)returnjson(oauth2.OAuth2PasswordBearerJsonRespModel.response_data(access_token=token).dict())oauth2_pb:oauth2.OAuth2PasswordBearer=oauth2.OAuth2PasswordBearer(route=oauth2_login,scopes={"user-info":"get all user info","user-name":"only get user name",},)defget_current_user(_oauth2_pb:"BaseOAuth2PasswordBearerProxy")->Callable[[str],User]:def_check_scope(token:str=Depends.i(_oauth2_pb))->User:user_model:Optional[User]=temp_token_dict.get(token,None)ifnotuser_model:raise_oauth2_pb.security.not_authenticated_excifnot_oauth2_pb.is_allow(user_model.scopes):raise_oauth2_pb.security.not_authenticated_excreturnuser_modelreturn_check_scope@pait()defoauth2_user_name(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-name"]))),)->HTTPResponse:returnjson({"code":0,"msg":"","data":user_model.name})@pait()asyncdefoauth2_user_info(user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-info"]))),)->HTTPResponse:returnjson({"code":0,"msg":"","data":user_model.dict()})app=Sanic(name="demo")app.add_route(oauth2_login,"/api/oauth2-login",methods={"POST"})app.add_route(oauth2_user_name,"/api/oauth2-user-name",methods={"GET"})app.add_route(oauth2_user_info,"/api/oauth2-user-info",methods={"GET"})AddDocRoute(app)if__name__=="__main__":importuvicornuvicorn.run(app)
importrandomimportstringfromtypingimportTYPE_CHECKING,Callable,Dict,List,OptionalfrompydanticimportBaseModel,Fieldfromtornado.ioloopimportIOLoopfromtornado.webimportApplication,HTTPError,RequestHandlerfrompait.app.tornadoimportpaitfrompait.app.tornado.securityimportoauth2frompait.fieldimportDependsfrompait.model.responseimportHttp400RespModelfrompait.openapi.doc_routeimportAddDocRouteifTYPE_CHECKING:frompait.app.base.security.oauth2importBaseOAuth2PasswordBearerProxyclassUser(BaseModel):uid:str=Field(...,description="user id")name:str=Field(...,description="user name")age:int=Field(...,description="user age")sex:str=Field(...,description="user sex")scopes:List[str]=Field(...,description="user scopes")temp_token_dict:Dict[str,User]={}classOAuth2LoginHandler(RequestHandler):@pait(response_model_list=[oauth2.OAuth2PasswordBearerJsonRespModel,Http400RespModel],)asyncdefpost(self,form_data:oauth2.OAuth2PasswordRequestFrom)->None:ifform_data.username!=form_data.password:raiseHTTPError(400)token:str="".join(random.choice(string.ascii_letters+string.digits)for_inrange(10))temp_token_dict[token]=User(uid="123",name=form_data.username,age=23,sex="M",scopes=form_data.scope)self.write(oauth2.OAuth2PasswordBearerJsonRespModel.response_data(access_token=token).dict())oauth2_pb:oauth2.OAuth2PasswordBearer=oauth2.OAuth2PasswordBearer(route=OAuth2LoginHandler.post,scopes={"user-info":"get all user info","user-name":"only get user name",},)defget_current_user(_oauth2_pb:"BaseOAuth2PasswordBearerProxy")->Callable[[str],User]:def_check_scope(token:str=Depends.i(_oauth2_pb))->User:user_model:Optional[User]=temp_token_dict.get(token,None)ifnotuser_model:raise_oauth2_pb.security.not_authenticated_excifnot_oauth2_pb.is_allow(user_model.scopes):raise_oauth2_pb.security.not_authenticated_excreturnuser_modelreturn_check_scopeclassOAuth2UserNameHandler(RequestHandler):@pait()defget(self,user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-name"]))))->None:self.write({"code":0,"msg":"","data":user_model.name})classOAuth2UserInfoHandler(RequestHandler):@pait()defget(self,user_model:User=Depends.t(get_current_user(oauth2_pb.get_depend(["user-info"]))))->None:self.write({"code":0,"msg":"","data":user_model.dict()})app:Application=Application([(r"/api/security/oauth2-login",OAuth2LoginHandler),(r"/api/security/oauth2-user-name",OAuth2UserNameHandler),(r"/api/security/oauth2-user-info",OAuth2UserInfoHandler),],)AddDocRoute(app)if__name__=="__main__":app.listen(8000)IOLoop.instance().start()
The first part is to create a Model -- User about the user's data and a temp_token_dict with key as token value as User to be used to mocker database storage.
The second part is to create a standard login route function that takes a parameter of type OAuth2PasswordRequestFrom,
which is Pait's encapsulation of Oauth2's login parameters, and which has the following source code:
can see that OAuth2PasswordRequestFrom inherits BaseModel and uses Form for the Field of all its parameters,
which means that its parameters get data from the form in the request body.
While the login route function simply checks the data after receiving it,
and returns a 400 response if the check is wrong.
If it passes, it generates a token and stores the token and User in temp_token_dict and returns the Oauth2 standard response via oauth2.OAuth2PasswordBearerJsonRespModel.
The third part is the creation of an instance of oauth2_pb via oauth2.OAuth2PasswordBearer as well as the creation of a function -- get-current_userto get the user.
The scopes parameter to create oauth2_pb is the permission description of oauth2_pb and the route parameter is the login route function,
when the route function registers with the web framework, oauth2_pb will find the URL of the route function and writes it to the tokenUrl attribute.
The get_current_user function will get the current user through the Token, and then through the is_allow method to determine whether the current user has permission to access the route function,
if not, then return a 403 response, if so, then return User Model.
Note that the get_current_user function receives the value of the oauth2.OAuth2PasswordBearer proxy class,
which already specifies only which permissions are allowed(by oauth2_pb.get_depend method).
In addition, the class has two functions, one that passes the requested parameters to the function via Depend,
and the other that provides the is_allow method for determining whether the user has permission to access the interface.
The fourth part is the route functions, which use the get_current_user function created in the third part.
where oauth2_pb.get_depend(["user-name"]) creates an instance of a proxy that only allows access with user-name privileges, via oauth2.OAuth2PasswordBearer.
oauth2_pb.get_depend(["user-info"]) will create a proxy instance that only allows access with user-info permissions via oauth2.OAuth2PasswordBearer.
The only difference between them is that the scopes are different.
After running the code, run the curl command to see them executed as follows:
The response result shows that a user with permission user-info can only access /api/oauth2-user-info,
while a user with permission user-name can only access /api/oauth2-user-name.
Note
The current version does not support refresh Url yet