Package: shieldapi
ShieldAPITheshieldapipackage provides utilities for implementing authentication/authorization mechanism through a Keycloak-backend in Python web applications, such as FastAPI and Flask. It includes functionality for managing user accounts, token authentication, and role-based access control.AuthorsYoav Nahshon (yoav.nahshon@iwm.fraunhofer.de)Pablo De Andres (pablo.de.andres@iwm.fraunhofer.de)Matthias Büschelberger (matthias.bueschelberger@iwm.fraunhofer.de)Treesa Joseph (treesa.joseph@sintef.no)Thomas Hagelien (thomas.f.hagelien@sintef.no)Simon AdorfInstallationYou can install the package using pip:pipinstallshieldapiPrerequisitesKeycloakIn order to set up Keycloak authentication for a Flask or FastAPI app using the shieldapi package, there are some prerequisites that must be met:Install Keycloak- Keycloak can be downloaded from the official Keycloak website. After downloading, follow the installation instructions to install Keycloak on your machine. Alternatively, you may use use aDocker image.Create a realm- A realm is a container for a set of users, authentication methods, and client applications. To create a realm in Keycloak, log in to the Keycloak Admin Console, click on "Add Realm", and fill in the necessary details. For more information on creating realms, see the official Keycloak documentation.Create a client- A client is an entity that can request authorization and/or authentication services from Keycloak. To create a client in Keycloak, go to the "Clients" tab in the Keycloak Admin Console, click "Create", and fill in the necessary details. For more information on creating clients, see the official Keycloak documentation.Create a client scope- A client scope defines a set of client-level roles that can be granted to users. To create a client scope in Keycloak, go to the "Client Scopes" tab in the Keycloak Admin Console, click "Create", and fill in the necessary details. For more information on creating client scopes, see the official Keycloak documentation.Create a user- A user is an entity that can authenticate to Keycloak and access client applications. To create a user in Keycloak, go to the "Users" tab in the Keycloak Admin Console, click "Add User", and fill in the necessary details. For more information on creating users, see the official Keycloak documentation.Create a role- A role is a set of permissions that can be granted to users or clients. To create a role in Keycloak, go to the "Roles" tab in the Keycloak Admin Console, click "Add Role", and fill in the necessary details. For more information on creating roles, see the official Keycloak documentation.Assign roles to client scope- To grant permissions to users or clients, roles must be assigned to client scopes. To assign roles to a client scope in Keycloak, go to the "Client Scopes" tab in the Keycloak Admin Console, click on the client scope you created in step 4, and then click on the "Mappings" tab. From there, you can assign roles to the client scope.Environmental variablesIn order to successfully enable the callback for the token-generation and token-verification, make sure that the following environmental variables are set correctly.Variable nameDescriptionExample valueKEYCLOAK_HOSTHostname of the the Keycloak instance"http://localhost:8080"KEYCLOAK_REALM_NAMERealm name you set under step 2 in the previous chapter"my_realm"KEYCLOAK_CLIENT_IDClient ID you created under step 3."shieldapi"KEYCLOAK_CLIENT_SECRETClient secret you created under step 3 (access_typeshould be set to "confidential")."1169a83e-a237-40ad-90db-8b8d4848de09"KEYCLOAK_VERIFY_HOSTControls whether SSL certificates are verified for HTTPS requests. Default is set toFalse."True"KEYCLOAK_REALM_ADMIN_USERUser name of the realm admin, if any action shall be performed as admin in your Flask/FastAPI application."admin"KEYCLOAK_REALM_ADMIN_PASSWORDPassword of the realm admin, if any action shall be performed as admin in your Flask/FastAPI application."admin_password"Usage with FlaskThere are several decorators which can be included into the Flask app for authentication of endpoints.A few examples are listed here:login_requiredTo use thelogin_requireddecorator, simply decorate any Flask route functions that require authentication. The decorator checks if the user is logged in by checking the access token, and aborts the request with a 401 Unauthorized error if the access token is not found or expired. If the access token is valid, the route function is executed normally.fromflaskimportFlaskfromshieldapi.frameworks.flaskimportlogin_requiredapp=Flask(__name__)@app.route('/protected_endpoint')@login_requireddefhello_world():return'Hello, World!'if__name__=='__main__':app.run()Run the flask app with aWSGIserver (recommended for production).If the service is up and running, access the endpoint of the service as a client:importrequestsACCESS_TOKEN="your_access_token_here"URL="http://localhost:5000/protected_endpoint"headers={"Authorization":f"Bearer{ACCESS_TOKEN}"}# GET request to the root endpointresponse=requests.get(URL,headers=headers)print(response.text)# prints 'Hello, World!'admin_requiredThis is a Flask decorator that checks if the user is an admin. The decorator extracts the access token from the request header and verifies that the token is valid and active. It then checks if the user has the "admin" role in the token's roles claim. If any of these checks fail, the decorator raises a 401 Unauthorized or a 403 Forbidden error.Example:fromflaskimportFlaskfromshieldapi.frameworks.flaskimportadmin_requiredapp=Flask(__name__)@app.route('/')defindex():return'Home Page'@app.route('/admin')@admin_requireddefadmin_view():return'Admin Dashboard'if__name__=='__main__':app.run(debug=True)In this example, theadmin_view()function can only be accessed by users who have the "admin" role in their access token. If a user without the "admin" role tries to access the view, a 403 Forbidden error will be raised.has_scopeThehas_scopefunction creates a decorator that limits access to the applications which have been granted the required scopes. The decorator takes in a list of required scopes and checks if the user's access token includes all of these scopes. If not, the user is denied access.To use this decorator, you can simply add the@has_scopedecorator above the route function for which you want to restrict access based on scopes. The decorator takes the list of required scopes as an argument.For example:fromflaskimportFlaskfromshieldapi.frameworks.flaskimporthas_scopeapp=Flask(__name__)@app.route('/admin')@has_scope(['admin'])defadmin():# code to handle admin requestsIn this example, theadmin()function will only be accessible to users who have been granted the "admin" scope in their access token. If the user's access token does not include the "admin" scope, the restricted_function in the decorator will return None and the user will be denied accessget_userinfoTheget_userinfofunction is used in a Flask application to retrieve the user information from an access token.To use this function, you should first obtain an access token, typically by using a decorator such asadmin_requiredorhas_scope, which would decorate the route or method where the user information is needed.Once the access token is obtained, you can call theget_userinfofunction to retrieve the user information from the token.The function returns a dictionary containing the user information, such as the user's email, name, and other attributes that were included in the access token.Usage with FastAPIThere are several FastAPI-dependency classes which are provided by the package.A few examples are listed here:AuthTokenBearerTo use theAuthTokenBearerHTTPBearer subclass in your FastAPI application, you can add it as a dependency to your API route functions that require authentication. Here's an example of how to use it:fromfastapiimportFastAPI,Dependsfromshieldapi.frameworks.fastapiimportAuthTokenBearerapp=FastAPI()# Create an instance of the AuthTokenBearer classauth_bearer=AuthTokenBearer()# Define a protected route that requires authentication@app.get("/protected_endpoint")asyncdefprotected_route(auth:str=Depends(auth_bearer)):# Use the authentication token to access protected resourcesreturn{"message":"This resource is protected"}In this example, theAuthTokenBearerinstance auth_bearer is added as a dependency to the protected_route function. This ensures that the route is protected and can only be accessed by authenticated users.When a request is made to the protected_route, FastAPI will call the__call__method of theAuthTokenBearerinstance to validate and extract the authentication token from the request header. If the token is missing or expired, an HTTPException will be raised.To use theAuthTokenBearersubclass with Keycloak authentication, you will need to configure your Keycloak server URL and realm name in theget_keycloak_openid()function in theshieldapi.keycloak_utilsmodule. Additionally, you will need to define the appropriate Keycloak roles and permissions in your application to control access to protected resources.Note that this subclass assumes that the authentication token is passed in the"Bearer {token}"format in the request header. If your application uses a different format, you may need to modify the__call__method to extract the token in the correct format.For exemplary usage, run the FastAPI app with aWSGIserver (recommended for production) or with aTestClient(recommended for production only).If the service is up and running, access the endpoint of the service as a client:importrequestsACCESS_TOKEN="your_access_token_here"URL="http://localhost:8080/protected_endpoint"headers={"Authorization":f"Bearer{ACCESS_TOKEN}"}# GET request to the root endpointresponse=requests.get(URL,headers=headers)print(response.json())# prints: {"message": "This resource is protected"}depends_auth_token_bearerAlternatively to theAuthTokenBearerabove, the helper functiondepends_auth_token_bearercan be used:fromfastapiimportFastAPI,Dependsfromshieldapi.frameworks.fastapiimportdepends_auth_token_bearerapp=FastAPI()# Define a protected route that requires authentication@app.get("/protected_endpoint")asyncdefprotected_route(auth:str=Depends(depends_auth_token_bearer)):# Use the authentication token to access protected resourcesreturn{"message":"This resource is protected"}BasicLoginCredentialsBasicLoginCredentialsis a subclass ofHTTPBasicCredentialsthat authenticates the user using Basic Authentication and retrieves an access token from Keycloak using theloginfunction. If the authentication credentials are valid, it returns the authentication token as a string in the"Bearer {token}"format.Here's an example of how to useBasicLoginCredentialsfor authentication in FastAPI:fromfastapiimportFastAPI,Depends,HTTPExceptionfromfastapi.securityimportHTTPBasicfromshieldapi.frameworks.fastapiimportBasicLoginCredentialsapp=FastAPI()security=HTTPBasic()@app.get("/protected_endpoint")asyncdefprotected_route(login:BasicLoginCredentials=Depends(security)):"""This route is protected and authentication with user/password in orderto generate a token."""ifnotlogin.token:raiseHTTPException(status_code=401,detail="Invalid authentication credentials")# Perform some action with the token herereturn{"message":"Authenticated successfully!"}In the example above, theprotected_endpointis protected by theBasicLoginCredentialsdependency. When a request is made to this route,FastAPIwill extract the Authorization header with the basic authentication credentials. TheBasicLoginCredentialsdependency will then authenticate with Keycloak using these credentials and return the authentication token as a string in the"Bearer {token}"format. If the authentication credentials are invalid, it will raise anHTTPExceptionwith status code 401 and the message"Invalid authentication credentials". Once the token is authenticated, the function can perform some action with the token before returning a response to the client.Note: The major difference betweenBasicLoginCredentialsandBasicLogin-dependency is that the latter one needs to be instantiated before injected as dependency into an endpoint or application.BasicLoginsimply returns astrwith the token whereasBasicLoginCredentialsreturn the full request-object with username and password.For exemplary usage, run the FstAPI app with aWSGIserver (recommended for production) or with aTestClient(recommended for production only).If the service is up and running, access the endpoint of the service as a client:importrequestsURL="http://localhost:8080/protected_endpoint"username="your_username"password="your_password"response=requests.get(URL,auth=(username,password))print(response.json())# prints: {"message": "Authenticated successfully!"}BasicLoginBasicLoginis a subclass ofHTTPBasicthat extracts the username and password from the HTTP basic authentication header, and uses them to get an access token from Keycloak using the login function. The returned access token is then used to authenticate the request.Example:fromfastapiimportFastAPI,Dependsfromshieldapi.frameworks.fastapiimportBasicLoginapp=FastAPI()security=BasicLogin()@app.get("/protected_endpoint")asyncdefget_user(token:str=Depends(security)):"""This route is protected and authentication with user/password in orderto generate a token."""ifnottoken:raiseHTTPException(status_code=401,detail="Invalid authentication credentials")# Perform some action with the token herereturn{"message":"Authenticated successfully!"}In this example,BasicLoginis used as a dependency of theprotected_endpointfunction. When the function is called,BasicLoginwill extract the basic auth credentials from the request header, pass them to the login function to get an access token from Keycloak, and return the access token as a string in the"Bearer {token}"format. The token parameter of the protected_route function will contain the authentication token as a string in the"Bearer {token}"format.For exemplary usage, run the FstAPI app with aWSGIserver (recommended for production) or with aTestClient(recommended for production only).If the service is up and running, access the endpoint of the service as a client:importrequestsURL="http://localhost:8080/protected_endpoint"username="your_username"password="your_password"response=requests.get(URL,auth=(username,password))print(response.json())# prints: {"message": "Authenticated successfully!"}depends_basic_loginAlternatively to theBasicLoginabove, the helper functiondepends_basic_logincan be used:fromfastapiimportFastAPI,Dependsfromshieldapi.frameworks.fastapiimportdepends_basic_loginapp=FastAPI()# Define a protected route that requires authentication@app.get("/protected_endpoint")asyncdefprotected_route(auth:str=Depends(depends_basic_login)):# Use the authentication token to access protected resourcesreturn{"message":"This resource is protected"}ContributingContributions in the form of issues, comments, and pull request are very welcome.To make code contributions, please fork this repository, and then create a pull request.
For development you will need to setup a Python environment (with a recent Python version), install the development requirements, and the pre-commit hooks with:pipinstall.[dev]pre-commitinstallTestsTests for this repository are implemented viapytest.
To run these tests, first install the test dependencies withpipinstall'.[tests]'and then run the tests with thepytestcommand.For maintainersTo create a new release, clone the repository, install development dependencies withpip install -e '.[dev]', and then executebumpver update --[major|minor|patch].
This will:Create a tagged release with bumped version and push it to the repository.Trigger a GitHub actions workflow that creates a GitHub release and publishes it on PyPI.Additional notes:The project follows semantic versioning.Use the--dryoption to preview the release change.AcknowledgementsThis work is supported by the MarketPlace project funded byHorizon 2020under the H2020-NMBP-25-2017 call (Grant No. 760173).LicenseThe code is licensed under BSD-3-Clause.
Copyright © 2023 Materials MarketPlace