--- title: Secure Flask (Python) API Application with ZITADEL description: "Secure Python Flask APIs with ZITADEL token validation and role-based access control." sidebar_label: Flask API --- This example shows you how to secure a Python3 Flask API with both authentication and authorization using ZITADEL. ## Overview ![](../../../public/img/python-flask/1.png) The Python API will have public, private, and private-scoped routes and check if a user is authenticated and authorized to access the routes. The private routes expect an authorization header with a valid access token in the request. The access token is used as a bearer token to authenticate the user when calling the API. The API will validate the access token on the [introspect endpoint](/apis/openidoauth/endpoints#introspection-endpoint) and will receive the user's roles from ZITADEL. The API application uses [Client Secret Basic](/apis/openidoauth/authn-methods#client-secret-basic) to authenticate against ZITADEL and access the introspection endpoint. You can use any valid access_token from a user or service account to send requests to the example API. In this example we will use a service account with a [personal access token](/guides/integrate/service-accounts/personal-access-token) which can be used directly to access the example API. ## Running the example ### Python Prerequisites In order to run the example you need to have `python3` and `pip3` installed. ### ZITADEL settings for the API ![](../../../public/img/python-flask/2.png) You need to setup a couple of things in ZITADEL. 1. If you don't have an instance yet, please go ahead and create an instance as explained [here](/guides/start/quickstart#3-create-your-zitadel-instance). Also, create a new project by following the steps [here](/guides/start/quickstart#4-create-your-project-and-application). 2. You must create an API application in your project. Follow [this guide](/guides/manage/console/applications-overview) to create a new application of type "API" with authentication method "Basic". Save both the ClientID and ClientSecret after you create the application. ### Create the API 1. Clone or download this [Python project](https://github.com/zitadel/example-api-python3-flask) to your workspace. ``` git clone https://github.com/zitadel/example-api-python3-flask cd example-api-python3-flask ``` 2. The [server.py](https://github.com/zitadel/example-api-python3-flask/blob/main/server.py) file contains a Flask-based API that provides authentication for routes using the OpenID Connect protocol as shown below. ```python from flask import Flask, jsonify, Response from authlib.integrations.flask_oauth2 import ResourceProtector from validator import ZitadelIntrospectTokenValidator, ValidatorError require_auth = ResourceProtector() require_auth.register_token_validator(ZitadelIntrospectTokenValidator()) APP = Flask(__name__) @APP.errorhandler(ValidatorError) def handle_auth_error(ex: ValidatorError) -> Response: response = jsonify(ex.error) response.status_code = ex.status_code return response @APP.route("/img/api/public") def public(): """No access token required.""" response = ( "Public route - You don't need to be authenticated to see this." ) return jsonify(message=response) @APP.route("/img/api/private") @require_auth(None) def private(): """A valid access token is required.""" response = ( "Private route - You need to be authenticated to see this." ) return jsonify(message=response) @APP.route("/img/api/private-scoped") @require_auth(["read:messages"]) def private_scoped(): """A valid access token and scope are required.""" response = ( "Private, scoped route - You need to be authenticated and have the role read:messages to see this." ) return jsonify(message=response) if __name__ == "__main__": APP.run() ``` The API has three routes: The [validator.py](https://github.com/zitadel/example-api-python3-flask/blob/main/validator.py) file implements the ZitadelIntrospectTokenValidator class, which is a custom class that inherits from the IntrospectTokenValidator class provided by the authlib library. The introspection process retrieves the token details from ZITADEL using ZITADEL's introspection endpoint. ```python from os import environ as env import os import time from typing import Dict from authlib.oauth2.rfc7662 import IntrospectTokenValidator import requests from dotenv import load_dotenv, find_dotenv from requests.auth import HTTPBasicAuth load_dotenv() ZITADEL_DOMAIN = os.getenv("ZITADEL_DOMAIN") CLIENT_ID = os.getenv("CLIENT_ID") CLIENT_SECRET = os.getenv("CLIENT_SECRET") class ValidatorError(Exception): def __init__(self, error: Dict[str, str], status_code: int): super().__init__() self.error = error self.status_code = status_code # Use Introspection in Resource Server # https://docs.authlib.org/en/latest/specs/rfc7662.html#require-oauth-introspection class ZitadelIntrospectTokenValidator(IntrospectTokenValidator): def introspect_token(self, token_string): url = f'{ZITADEL_DOMAIN}/oauth/v2/introspect' data = {'token': token_string, 'token_type_hint': 'access_token', 'scope': 'openid'} auth = HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) resp = requests.post(url, data=data, auth=auth) resp.raise_for_status() return resp.json() def match_token_scopes(self, token, or_scopes): if or_scopes is None: return True roles = token["urn:zitadel:iam:org:project:roles"].keys() for and_scopes in or_scopes: scopes = and_scopes.split() """print(f"Check if all {scopes} are in {roles}")""" if all(key in roles for key in scopes): return True return False def validate_token(self, token, scopes, request): print (f"Token: {token}\n") now = int( time.time() ) if not token: raise ValidatorError({ "code": "invalid_token_revoked", "description": "Token was revoked." }, 401) """Expired""" if token["exp"] < now: raise ValidatorError({ "code": "invalid_token_expired", "description": "Token has expired." }, 401) """Revoked""" if not token["active"]: raise InvalidTokenError() """Insufficient Scope""" if not self.match_token_scopes(token, scopes): raise ValidatorError({ "code": "insufficient_scope", "description": f"Token has insufficient scope. Route requires: {scopes}" }, 401) def __call__(self, *args, **kwargs): res = self.introspect_token(*args, **kwargs) return res ``` 3. Create a new file named ".env" in the directory. Copy the settings in the [".env.example"](https://github.com/zitadel/example-api-python3-flask/blob/main/.env.example) file to the newly created .env file. Set the values with your Custom Domain/Issuer URL, Client ID, and Client Secret from the previous steps. Obtain your Issuer URL by following [these steps](/guides/start/quickstart#5-collect-your-integration-keys). ```python ZITADEL_DOMAIN = "https://custom-domain-abcdef.zitadel.cloud" CLIENT_ID = "197....@projectname" CLIENT_SECRET = "NVAp70IqiGmJldbS...." ``` ### ZITADEL settings to create a service account ![](../../../public/img/python-flask/3.png) 1. Create a service account and a Personal Access Token (PAT) for that user by following [this guide](/guides/integrate/service-accounts/personal-access-token#create-a-service-account-with-a-pat). 2. To assign roles, follow [this guide](/guides/manage/console/roles) to create a role `read:messages` on your project. 3. Next, add the role `read:messages` to the service account you created. Follow this [guide](/guides/manage/console/roles#role-assignments) for more information on creating a role assignment. ### Run the API 1. Install required dependencies by running `pip3 install -r requirements.txt` on your terminal. 2. Run the API with the `python3 server.py` command. 3. Open another terminal and follow the next step to test the API. ## Test the API ### Public route Invoke the public route by running the following command: ``` curl --request GET \ --url http://127.0.0.1:5000/api/public ``` You should get a response with Status Code 200 and the following message. `{"message":"Public route - You don't need to be authenticated to see this."}` ### Private route Call the private route without authorization headers by running the following command: ``` curl --request GET \ --url http://127.0.0.1:5000/api/private ``` You should get a response with Status Code 401 and an error message. Now let's add an authorization header to your request. Save the personal access token for your service account to a variable by running the following command. Replace the value with the PAT you obtained earlier. `PAT=nr9vnUTkQkn4rxWk...` Then call the private route with the PAT in the authorization header. ``` curl --request GET \ --url http://127.0.0.1:5000/api/private \ --header "authorization: Bearer $PAT" ``` Now you should get a response with Status Code 200 and the following message. `{"message":"Private route - You need to be authenticated to see this."}` ### Private route, protected Call the private route that requires the user to have a certain role ``` curl --request GET \ --url http://127.0.0.1:5000/api/private-scoped \ --header "authorization: Bearer $PAT" ``` You should get a response with Status Code 200 and the following message. `{"message":"Private, scoped route - You need to be authenticated and have the role read:messages to see this."}` You can remove the role from the service account in ZITADEL and try again. You should then get a Status Code 403, Forbidden error.