mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
## Todos for release - [x] Configure Env in docs project on vercel - [x] Configure Root Path in the docs project on vercel - [ ] Remove old CSP https://github.com/zitadel/website/pull/1592 ## What we did This pull request migrates the project documentation from the old `docs/` directory to the new `apps/docs/` directory, introduces a new documentation system built with Next.js and Fumadocs, and updates all relevant references, configuration files, and documentation to reflect this change. It also adds new configuration and ignore files for the new documentation app, updates CI and linting to exclude the new docs from certain checks, and revises the contributing guidelines accordingly. **Documentation System Migration and New Docs App** * Migrated all documentation from `docs/` to `apps/docs/`, and updated all references in `README.md`, `CONTRIBUTING.md`, and other files to point to the new location. [[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640) [[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607) [[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687) [[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3) [[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30) * Added a new Next.js/Fumadocs-based documentation app under `apps/docs/`, including core app files, layouts, routing, search API, and a comprehensive `README.md` with development and contribution instructions. [[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74) [[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39) [[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7) [apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81), [apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79), [[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7) [[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29) [[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25) **Configuration and Tooling Updates** * Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and `.github/dependabot.yml` to properly handle the new docs app: excluded `apps/docs` from certain checks, added npm dependency updates for the docs app, and excluded generated content. [[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14) [[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6) [[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129) [[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106) * Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3 version for consistency. **Licensing and Miscellaneous** * Added `apps/docs/` to the list of licensed directories in `LICENSING.md`. These changes ensure the documentation is now maintained in a modern, scalable system and all project tooling is updated to support the new structure. --------- Co-authored-by: Federico Coppede <[email protected]>
256 lines
9.8 KiB
Plaintext
256 lines
9.8 KiB
Plaintext
---
|
|
title: ZITADEL with Python
|
|
sidebar_label: Python
|
|
---
|
|
|
|
This example shows you how to secure a Python3 Flask API with both authentication and authorization using ZITADEL.
|
|
|
|
## Overview
|
|
|
|

|
|
|
|
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-users/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 configuration for the API
|
|
|
|

|
|
|
|
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:
|
|
|
|
<ul>
|
|
<li> "/img/api/public" - No access token is required.</li>
|
|
<li>"/img/api/private" - A valid access token is required.</li>
|
|
<li>"/img/api/private-scoped" - A valid access token and a "read:messages" scope are required.</li>
|
|
</ul>
|
|
|
|
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 configuration 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 configuration to create a service user
|
|
|
|

|
|
|
|
1. Create a service user and a Personal Access Token (PAT) for that user by following [this guide](/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
|
|
2. To enable authorization, follow [this guide](/guides/manage/console/roles) to create a role `read:messages` on your project.
|
|
3. Next, create an authorization for the service user you created by adding the role `read:messages` to the user. Follow this [guide](/guides/manage/console/roles#authorizations) for more information on creating an authorization.
|
|
|
|
### 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 user 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 user in ZITADEL and try again. You should then get a Status Code 403, Forbidden error.
|