mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-21 06:54:25 +00:00
docs: introduce versioned docs and migrate to fuma (#11166)
## 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]>
This commit is contained in:
co-authored by
Federico Coppede
parent
7b6395c348
commit
d8ea041c56
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: ZITADEL with Go
|
||||
sidebar_label: Go
|
||||
---
|
||||
|
||||
This integration guide shows you how to integrate **ZITADEL** into your Go API. It demonstrates how to secure your API using
|
||||
OAuth 2 Token Introspection.
|
||||
|
||||
> ℹ️ These examples and guides are based on our official [Go SDK](https://github.com/zitadel/zitadel-go).
|
||||
>
|
||||
> The SDK is a convenient wrapper around our low-level [OIDC library](https://github.com/zitadel/oidc). For most use cases, using the helpers provided in our [Go SDK](https://github.com/zitadel/zitadel-go) is the recommended approach for implementing authentication.
|
||||
|
||||
At the end of the guide you should have an API with a protected endpoint.
|
||||
|
||||
> This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [Go SDK](https://github.com/zitadel/zitadel-go).
|
||||
All that is required, is to create your API, create a private key and a personal access token for a service user.
|
||||
|
||||
### Set up application and obtain keys
|
||||
|
||||
Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console.
|
||||
You'll need to provide some information about your app. We recommend creating a new app to start from scratch.
|
||||
|
||||
Starting from the homepage of your management console, click on Create Application
|
||||
|
||||

|
||||
|
||||
Select a project from the dropdown and select *Other* as framework, then continue.
|
||||
|
||||

|
||||
|
||||
Add your app name and select *API* as application type, then continue.
|
||||
|
||||

|
||||
|
||||
We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. So select *JWT* as authentication method
|
||||
|
||||

|
||||
|
||||
You then need to create a new JSON key.
|
||||
|
||||

|
||||
|
||||
Select an expiration date that suits you.
|
||||
|
||||

|
||||
|
||||
And make sure to download it, as you won't be able to retrieve it again.
|
||||
|
||||

|
||||
|
||||
Now we need to create a *Personal Access Token* to authenticate the client requests.
|
||||
|
||||
On the user view, switch to *Service Users* and create a new one.
|
||||
|
||||

|
||||
|
||||
Give the service user a name and a user name. Select `Bearer` as *Access Token Type*.
|
||||
|
||||

|
||||
|
||||
### Create service user and personal access token (PAT)
|
||||
|
||||
Once done, from the left panel of the user management, click on Personal Access Token and create a new one.
|
||||
|
||||

|
||||
|
||||
Set an expiration date and then copy the PAT generated to somewhere safe. We will need it later.
|
||||
|
||||

|
||||
|
||||
## Go Setup
|
||||
|
||||
### Add Go SDK to your project
|
||||
|
||||
You need to add the [SDK](https://github.com/zitadel/zitadel-go) into Go Modules by:
|
||||
|
||||
```bash
|
||||
go get -u github.com/zitadel/zitadel-go/v3
|
||||
```
|
||||
|
||||
### Create example API
|
||||
|
||||
Create a new go file with the content below. This will create an API with three endpoints:
|
||||
- `/api/healthz`: can be called by anyone and always returns `OK`
|
||||
- `/api/tasks`: requires authorization and returns the available tasks
|
||||
- `/api/add-task`: requires authorization with granted `admin` role and adds the task to the list
|
||||
|
||||
If authorization is required, the token must not be expired and the API has to be part of the audience (either client_id or project_id).
|
||||
|
||||
For tests we will use a Personal Access Token.
|
||||
|
||||
```go reference
|
||||
https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go
|
||||
```
|
||||
|
||||
You will need to provide some values for the program to run:
|
||||
- `domain`: Your ZITADEL Custom Domain, e.g. https://my-domain.zitadel.cloud
|
||||
- `key`: The path to the downloaded key.json
|
||||
- `port`: The port on which the API will be accessible, default it 8089
|
||||
|
||||
## Test API
|
||||
|
||||
After you have configured everything correctly, you can simply start the example by:
|
||||
|
||||
```bash
|
||||
go run main.go --domain <custom domain> --key <path>
|
||||
```
|
||||
|
||||
This could look like:
|
||||
|
||||
```bash
|
||||
go run main.go --domain my-domain.zitadel.cloud --key ./api.json
|
||||
```
|
||||
|
||||
After you get a successful log:
|
||||
```
|
||||
2023/12/04 10:27:42 INFO server listening, press ctrl+c to stop addr=http://localhost:8089
|
||||
```
|
||||
|
||||
### Public endpoint
|
||||
|
||||
Now you can call the API by browser or curl. Try the healthz endpoint first:
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:8089/api/healthz
|
||||
```
|
||||
|
||||
it should return something like:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 09:29:38 GMT
|
||||
Content-Length: 4
|
||||
|
||||
"OK"
|
||||
```
|
||||
|
||||
### Task list
|
||||
|
||||
and the task list endpoint:
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
it will return:
|
||||
|
||||
```
|
||||
HTTP/1.1 401 Unauthorized
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Mon, 04 Dec 2023 09:41:54 GMT
|
||||
Content-Length: 44
|
||||
|
||||
unauthorized: authorization header is empty
|
||||
```
|
||||
|
||||
We need to use the personal access token generated previously.
|
||||
|
||||
If you provide a valid Bearer Token:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
it will return an empty list:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 09:49:06 GMT
|
||||
Content-Length: 2
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
### Try to add a new task
|
||||
|
||||
Let's see what happens if you call the AddTask endpoint:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task
|
||||
```
|
||||
|
||||
it will complain about the missing `admin` role:
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
X-Content-Type-Options: nosniff
|
||||
Date: Mon, 04 Dec 2023 09:52:00 GMT
|
||||
Content-Length: 50
|
||||
|
||||
permission denied: missing required role: `admin`
|
||||
```
|
||||
|
||||
### Add administrator role
|
||||
|
||||
So let's create the role and grant it to the user. To do so, go to your project in ZITADEL Management Console
|
||||
and create the role by selecting `Roles` in the navigation and then clicking on the `New Role` button.
|
||||
Finally, create the role as shown below:
|
||||
|
||||

|
||||
|
||||
After you have created the role, let's grant it the user, who requested the tasks.
|
||||
Click on `Authorization` in the navigation and create a new one by selecting the user and the `admin` role.
|
||||
After successful creation, it should look like:
|
||||
|
||||

|
||||
|
||||
So you should now be able to add a new task:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task --data "task=My new task"
|
||||
```
|
||||
|
||||
which will report back the successful addition:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 10:06:29 GMT
|
||||
Content-Length: 26
|
||||
|
||||
"task `My new task` added"
|
||||
```
|
||||
|
||||
Let's now retrieve the task list again:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks
|
||||
```
|
||||
|
||||
As you can see your new task ist listed. And since you're an `admin` now, you will always get an additional `create a new task on /api/add-task`:
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Date: Mon, 04 Dec 2023 10:08:38 GMT
|
||||
Content-Length: 62
|
||||
|
||||
{"tasks":["My new task","create a new task on /api/add-task"]}
|
||||
```
|
||||
@@ -0,0 +1,283 @@
|
||||
---
|
||||
title: ZITADEL with Java Spring Boot
|
||||
sidebar_label: Java Spring Boot
|
||||
---
|
||||
|
||||
This integration guide shows you how to integrate **ZITADEL** into your Java Spring Boot API. It demonstrates how to secure your API using
|
||||
OAuth 2 Token Introspection.
|
||||
|
||||
At the end of the guide you should have an API with a protected endpoint.
|
||||
|
||||
<Callout>
|
||||
This documentation references our [example](https://github.com/zitadel/zitadel-java) on GitHub.
|
||||
You can either create your own application or directly run the example by providing the necessary arguments.
|
||||
</Callout>
|
||||
|
||||
## Set up application
|
||||
|
||||
Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console.
|
||||
You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page.
|
||||
Select the **API** application type and continue.
|
||||
|
||||

|
||||
|
||||
Select Basic Auth for authenticating at the Introspection Endpoint.
|
||||
|
||||

|
||||
|
||||
After successful creation of the app, a pop-up will appear displaying the app's client ID. Copy the client ID and secret, as you will need it to configure your Java client.
|
||||
|
||||

|
||||
|
||||
## Spring Setup
|
||||
|
||||
Now that you have configured your web application on the ZITADEL side, you can proceed with the integration of your Spring client.
|
||||
This guide will reference the [example repository](https://github.com/zitadel/zitadel-java) and explain the necessary steps taken in there.
|
||||
If your starting from scratch, you can use the Spring Initializer with the [following setup](https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.2.1&packaging=jar&jvmVersion=17&dependencies=web,lombok,oauth2-resource-server) as a base.
|
||||
|
||||
### Support class
|
||||
|
||||
To be able to take the most out of ZITADELs RBAC, we first need to create a CustomAuthorityOpaqueTokenIntrospector, that will
|
||||
customize the introspection behavior and map the role claims (`urn:zitadel:iam:org:project:roles`)
|
||||
into Spring Security `authiorities`, which can be used later on to determine the granted permissions.
|
||||
|
||||
So in your application, create a `support/zitadel` package and in there the `CustomAuthorityOpaqueTokenIntrospector.java`:
|
||||
|
||||
```java reference
|
||||
https://github.com/zitadel/zitadel-java/blob/main/api/src/main/java/demo/app/support/zitadel/CustomAuthorityOpaqueTokenIntrospector.java
|
||||
```
|
||||
|
||||
### Application server configuration
|
||||
|
||||
As we have now our support class, we can now create and configure the application server itself.
|
||||
|
||||
In a new `config` package, create the `WebSecurityConfig.java`.
|
||||
This class will take care of the authorization by require the calls on `/api/tasks` to be authorized. Any other endpoint will be public by default.
|
||||
It will also use the just created CustomAuthorityOpaqueTokenIntrospector for the introspection call:
|
||||
|
||||
```java reference
|
||||
https://github.com/zitadel/zitadel-java/blob/main/api/src/main/java/demo/app/config/WebSecurityConfig.java
|
||||
```
|
||||
|
||||
For the authorization (and the server in general) to work, the application needs some configuration, so please provide the following to your `application.yml` (resources folder):
|
||||
|
||||
```yaml reference
|
||||
https://github.com/zitadel/zitadel-java/blob/main/api/src/main/resources/application.yml
|
||||
```
|
||||
|
||||
Note that the `introspection-uri`, `client-id` and `client-secret` are only placeholders. You can either change them in here using the values provided by ZITADEL
|
||||
or pass them later on as arguments when starting the application.
|
||||
|
||||
### Create example API
|
||||
|
||||
Create a `api` package with a `ExampleController.java` file with the content below. This will create an API with three endpoints / methods:
|
||||
- `/api/healthz`: can be called by anyone and always returns `OK`
|
||||
- `/api/tasks (GET)`: requires authorization and returns the available tasks
|
||||
- `/api/tasks (POST)`: requires authorization with granted `admin` role and adds the task to the list
|
||||
|
||||
If authorization is required, the token must not be expired and the API has to be part of the audience (either client_id or project_id).
|
||||
|
||||
For tests we will use a Personal Access Token or the [Java Spring web example](../login/java-spring).
|
||||
|
||||
```java reference
|
||||
https://github.com/zitadel/zitadel-java/blob/main/api/src/main/java/demo/app/api/ExampleController.java
|
||||
```
|
||||
|
||||
## Test API
|
||||
|
||||
In case you've created your own application and depending on your development setup you might need to build the application first:
|
||||
|
||||
```bash
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
You will need to provide the `introspection-uri` ($CUSTOM_DOMAIN/oauth/v2/introspect), the `client-id` and `client-secret` previously created:
|
||||
|
||||
```bash
|
||||
java \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.introspection-uri=<see configuration above> \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.client-id=<see configuration above> \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.client-secret=<see configuration above> \
|
||||
-jar api/target/api-0.0.2-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
This could look like:
|
||||
|
||||
```bash
|
||||
java \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://my-domain.zitadel.cloud/oauth/v2/introspect \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.client-id=243861220627644836@example \
|
||||
-Dspring.security.oauth2.resourceserver.opaquetoken.client-secret=WJKLF3kfPOi3optkg9vi3jmfjv8oj32nfiäohj!FSC09RWUSR \
|
||||
-jar web/target/web-0.0.2-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
### Public endpoint
|
||||
|
||||
Now you can call the API by browser or curl. Try the healthz endpoint first:
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:18090/api/healthz
|
||||
```
|
||||
|
||||
it should return something like:
|
||||
|
||||
```
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
Content-Length: 2
|
||||
Date: Mon, 15 Jan 2024 09:07:21 GMT
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
### Task list
|
||||
|
||||
and the task list endpoint:
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:18090/api/tasks
|
||||
```
|
||||
|
||||
it will return:
|
||||
|
||||
```
|
||||
HTTP/1.1 401
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
WWW-Authenticate: Bearer
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Length: 0
|
||||
Date: Mon, 15 Jan 2024 09:07:55 GMT
|
||||
```
|
||||
|
||||
Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience
|
||||
(e.g. by using the [Spring Boot web example](../login/java-spring)) use a PAT of a service account.
|
||||
|
||||
If you provide a valid Bearer Token:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:18090/api/tasks
|
||||
```
|
||||
|
||||
it will return an empty list:
|
||||
```
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: application/json
|
||||
Transfer-Encoding: chunked
|
||||
Date: Mon, 15 Jan 2024 09:15:10 GMT
|
||||
|
||||
[]
|
||||
```
|
||||
|
||||
### Try to add a new task
|
||||
|
||||
Let's see what happens if you call the tasks endpoint:
|
||||
|
||||
```bash
|
||||
curl -i -X POST -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" --data 'my new task' http://localhost:18090/api/tasks
|
||||
```
|
||||
|
||||
it will complain with a permission denied (missing `admin` role):
|
||||
```
|
||||
HTTP/1.1 403
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Length: 0
|
||||
Date: Mon, 15 Jan 2024 09:24:39 GMT
|
||||
```
|
||||
|
||||
### Add administrator role
|
||||
|
||||
So let's create the role and grant it to the user. To do so, go to your project in ZITADEL Management Console
|
||||
and create the role by selecting `Roles` in the navigation and then clicking on the `New Role` button.
|
||||
Finally, create the role as shown below:
|
||||
|
||||

|
||||
|
||||
After you have created the role, let's grant it the user, who requested the tasks.
|
||||
Click on `Authorization` in the navigation and create a new one by selecting the user and the `admin` role.
|
||||
After successful creation, it should look like:
|
||||
|
||||

|
||||
|
||||
So you should now be able to add a new task:
|
||||
|
||||
```bash
|
||||
curl -i -X POST -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" --data 'my new task' http://localhost:18090/api/tasks
|
||||
```
|
||||
|
||||
which will report back the successful addition:
|
||||
```
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: application/json
|
||||
Content-Length: 10
|
||||
Date: Mon, 15 Jan 2024 09:26:11 GMT
|
||||
|
||||
task added
|
||||
```
|
||||
|
||||
Let's now retrieve the task list again:
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer ${token}" http://localhost:18090/api/tasks
|
||||
```
|
||||
|
||||
As you can see your new task is listed:
|
||||
```
|
||||
HTTP/1.1 200
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: application/json
|
||||
Transfer-Encoding: chunked
|
||||
Date: Mon, 15 Jan 2024 09:26:48 GMT
|
||||
|
||||
["my new task"]
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: ZITADEL with Node.js
|
||||
sidebar_label: NestJS
|
||||
---
|
||||
|
||||
# ZITADEL with Node.js (NestJS)
|
||||
|
||||
This documentation section guides you through the process of integrating ZITADEL into your Node.js backend using the NestJS framework. The provided example demonstrates authentication using an OIDC (OAuth2) token introspection strategy with a ZITADEL service account for machine-to-machine communication.
|
||||
|
||||
## Overview
|
||||
|
||||
The NestJS API includes a single secured route that prints "Hello World!" when authenticated. The API expects an authorization header with a valid JWT, serving 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 receive the user from ZITADEL.
|
||||
|
||||
The API application utilizes [JWT with Private Key](/apis/openidoauth/authn-methods#jwt-with-private-key) for authentication against ZITADEL and accessing the introspection endpoint. Make sure to create an API Application within Zitadel and download the JSON. In this instance, we use this service account, so make sure to provide the secrets in the example application via environmental variables.
|
||||
|
||||
## Overview
|
||||
|
||||
The NestJS API includes a private endpoint `GET http://localhost:${APP_PORT}/api/v1/app`, which returns "Hello World" when authenticated. The authentication is performed using a JWT obtained through the token introspection strategy.
|
||||
|
||||
## Running the Example
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Make sure you have Node.js and npm installed on your machine.
|
||||
|
||||
### ZITADEL Configuration for the API
|
||||
|
||||
1. Create a ZITADEL instance and a project by following the steps [here](/guides/start/quickstart#3-create-your-zitadel-instance).
|
||||
|
||||
2. Set up an API application within your project:
|
||||
- Create a new application of type "API" with authentication method "Private Key".
|
||||
- Create a and save the Private Key JSON file.
|
||||
|
||||
### Create and Run the API
|
||||
|
||||
Clone or download the [example repository](https://github.com/ehwplus/zitadel-nodejs-nestjs):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ehwplus/zitadel-nodejs-nestjs && cd zitadel-nodejs-nestjs
|
||||
```
|
||||
|
||||
and follow the instructions here: https://github.com/ehwplus/zitadel-nodejs-nestjs/blob/main/README.md#installation
|
||||
|
||||
### Test the API
|
||||
|
||||
Call the API without authorization headers:
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url http://localhost:${APP_PORT}/api/v1/app
|
||||
```
|
||||
|
||||
You should get a response with Status Code 401 and an error message.
|
||||
|
||||
Now, add an authorization header with a valid JWT obtained through ZITADEL:
|
||||
|
||||
```bash
|
||||
export JWT=your-valid-jwt
|
||||
|
||||
curl --request GET \
|
||||
--url http://localhost:${APP_PORT}/api/v1/app \
|
||||
--header "authorization: Bearer $JWT"
|
||||
```
|
||||
|
||||
You should now receive a response with Status Code 200 and the message:
|
||||
|
||||
```json
|
||||
"Hello World!"
|
||||
```
|
||||
|
||||
Congratulations! You have successfully integrated ZITADEL authentication into your NestJS API using the Token Introspection strategy.
|
||||
@@ -0,0 +1,306 @@
|
||||
---
|
||||
title: ZITADEL with Pylon
|
||||
sidebar_label: Pylon
|
||||
---
|
||||
|
||||
import AppJWT from "../imports/_app_jwt.mdx";
|
||||
import ServiceuserJWT from "../imports/_serviceuser_jwt.mdx";
|
||||
import ServiceuserRole from "../imports/_serviceuser_role.mdx";
|
||||
|
||||
This integration guide demonstrates the recommended way to incorporate ZITADEL into your [Pylon](https://pylon.cronit.io) service.
|
||||
It explains how to check the token validity in the API and how to check for permissions.
|
||||
|
||||
By the end of this guide, your application will have three different endpoint which are public, private(valid token) and private-scoped(valid token with specific role).
|
||||
|
||||
## ZITADEL setup
|
||||
|
||||
Before we can start building our application, we have to do a few configuration steps in ZITADEL Management Console.
|
||||
|
||||
### Create application
|
||||
|
||||
<AppJWT components={props.components} />
|
||||
|
||||
### Create Serviceuser
|
||||
|
||||
<ServiceuserJWT components={props.components} />
|
||||
|
||||
### Give Serviceuser an authorization
|
||||
|
||||
<ServiceuserRole components={props.components} />
|
||||
|
||||
### Prerequisites
|
||||
|
||||
At the end you should have the following for the API:
|
||||
|
||||
- Issuer, something like `https://example.zitadel.cloud` or `http://localhost:8080`
|
||||
- `.json`-key-file for the API, from the application
|
||||
- ID of the project
|
||||
|
||||
And the following from the Serviceuser:
|
||||
|
||||
- `.json`-key-file from the serviceuser
|
||||
|
||||
## Setup new Pylon service
|
||||
|
||||
Pylon allows you to create a new service using the `npm create pylon` command. This command creates a new Pylon project with a basic project structure and configuration.
|
||||
During the setup process, you can choose your preferred runtime, such as Bun, Node.js, or Cloudflare Workers.
|
||||
|
||||
**This guide uses the Bun runtime.**
|
||||
|
||||
### Creating a new project
|
||||
|
||||
To create a new Pylon project, run the following command:
|
||||
|
||||
```bash
|
||||
npm create pylon my-pylon@latest
|
||||
```
|
||||
|
||||
This will create a new directory called `my-pylon` with a basic Pylon project structure.
|
||||
|
||||
### Project structure
|
||||
|
||||
Pylon projects are structured as follows:
|
||||
|
||||
```
|
||||
my-pylon/
|
||||
├── .pylon/
|
||||
├── src/
|
||||
│ ├── index.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
```
|
||||
|
||||
- `.pylon/`: Contains the production build of your project.
|
||||
- `src/`: Contains the source code of your project.
|
||||
- `src/index.ts`: The entry point of your Pylon service.
|
||||
- `package.json`: The npm package configuration file.
|
||||
- `tsconfig.json`: The TypeScript configuration file.
|
||||
|
||||
### Basic example
|
||||
|
||||
Here's an example of a basic Pylon service:
|
||||
|
||||
```ts
|
||||
|
||||
export const graphql = {
|
||||
Query: {
|
||||
sum: (a: number, b: number) => a + b,
|
||||
},
|
||||
Mutation: {
|
||||
divide: (a: number, b: number) => a / b,
|
||||
},
|
||||
};
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
## Secure the API
|
||||
|
||||
### Add ZITADEL info to the service
|
||||
|
||||
1. Create a `.env` file in the root folder of your project and add the following configuration:
|
||||
|
||||
```bash
|
||||
AUTH_ISSUER='URL to the zitadel instance'
|
||||
AUTH_PROJECT_ID='ID of the project'
|
||||
```
|
||||
|
||||
It should look something like this:
|
||||
|
||||
```bash
|
||||
AUTH_ISSUER='https://example.zitadel.cloud'
|
||||
AUTH_PROJECT_ID='250719519163548112'
|
||||
```
|
||||
|
||||
2. Copy the `.json`-key-file that you downloaded from the ZITADEL Management Console into the root folder of your project and rename it to `key.json`.
|
||||
|
||||
3. (Optional) For added convenience in production environments, you can include the content of the .json key file as `AUTH_KEY` in the .env file or as an environment variable.
|
||||
|
||||
### Auth
|
||||
|
||||
Pylon provides a auth module and a decorator to check the validity of the token and the permissions.
|
||||
|
||||
- `auth.initialize()`: Initializes the authentication middleware.
|
||||
- `auth.require()` : Middleware to check if the token is valid.
|
||||
- `auth.require({roles: ['role']})`: Middleware to check if the token is valid and has the specified roles.
|
||||
- `requireAuth()`: Decorator to check if the token is valid.
|
||||
- `requireAuth({roles: ['role']})`: Decorator to check if the token is valid and has the specified roles.
|
||||
|
||||
### Build the Pylon service
|
||||
|
||||
Now we will create a new Pylon service with the following endpoints:
|
||||
|
||||
- `/api/public`: Public endpoint
|
||||
- `/api/private`: Private endpoint
|
||||
- `/api/private-scoped`: Private endpoint with specific role
|
||||
- `/graphql`: GraphQL endpoint
|
||||
- Query: `me`: Private endpoint that returns the current user and the messages if the role is `read:messages`
|
||||
- Query: `info`: Public endpoint
|
||||
|
||||
### Create the service
|
||||
|
||||
The following code demonstrates how to create a Pylon service with the required endpoints, it must be added to the `src/index.ts` file of your project:
|
||||
|
||||
```ts
|
||||
import {
|
||||
app,
|
||||
auth,
|
||||
requireAuth,
|
||||
getContext,
|
||||
ServiceError,
|
||||
} from "@getcronit/pylon";
|
||||
|
||||
class User {
|
||||
id: string;
|
||||
name: string;
|
||||
#messages: string[];
|
||||
|
||||
constructor(id: string, name: string, messages: string[]) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.#messages = messages;
|
||||
}
|
||||
|
||||
@requireAuth({ roles: ["read:messages"] })
|
||||
async messages() {
|
||||
return this.#messages;
|
||||
}
|
||||
|
||||
static users: User[] = [];
|
||||
|
||||
@requireAuth()
|
||||
static async me() {
|
||||
const ctx = getContext();
|
||||
const id = ctx.get("auth")!.sub;
|
||||
|
||||
const user = User.users.find((user) => user.id === id);
|
||||
|
||||
if (!user) {
|
||||
throw new ServiceError("User not found", {
|
||||
statusCode: 404,
|
||||
code: "USER_NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@requireAuth()
|
||||
static async create() {
|
||||
const ctx = getContext();
|
||||
|
||||
const auth = ctx.get("auth")!;
|
||||
|
||||
// Check if the user already exists
|
||||
|
||||
if (User.users.find((user) => user.id === auth.sub)) {
|
||||
throw new ServiceError("User already exists", {
|
||||
statusCode: 400,
|
||||
code: "USER_ALREADY_EXISTS",
|
||||
});
|
||||
}
|
||||
|
||||
const user = new User(auth.sub, auth.username || "unknown", [
|
||||
"Welcome to Pylon with ZITADEL!",
|
||||
]);
|
||||
|
||||
User.users.push(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
export const graphql = {
|
||||
Query: {
|
||||
me: User.me,
|
||||
info: () => "Public Data",
|
||||
},
|
||||
Mutation: {
|
||||
createUser: User.create,
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize the authentication middleware
|
||||
app.use("*", auth.initialize());
|
||||
|
||||
// Automatically try to create a user for each request for demonstration purposes
|
||||
app.use(async (_, next) => {
|
||||
try {
|
||||
await User.create();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
// Fail silently if the user already exists
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.get("/img/api/info", (c) => {
|
||||
return new Response("Public Data");
|
||||
});
|
||||
|
||||
// The `auth.require()` middleware is optional here, as the `User.me` method already checks for it.
|
||||
app.get("/img/api/me", auth.require(), async (c) => {
|
||||
const user = await User.me();
|
||||
|
||||
return c.json(user);
|
||||
});
|
||||
|
||||
// A role check for `read:messages` is not required here, as the `user.messages` method already checks for it.
|
||||
app.get("/img/api/me/messages", auth.require(), async (c) => {
|
||||
const user = await User.me();
|
||||
|
||||
// This will throw an error if the user does not have the `read:messages` role
|
||||
return c.json(await user.messages());
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
### Call the API
|
||||
|
||||
To call the API you need an access token, which is then verified by ZITADEL.
|
||||
Please follow [this guide here](/guides/integrate/token-introspection/private-key-jwt), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
|
||||
|
||||
<Callout>
|
||||
You can also create a PAT for the serviceuser and use it to test the API. For this, follow [this guide](/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
|
||||
</Callout>
|
||||
|
||||
Optionally set the token as an environment variable:
|
||||
|
||||
```
|
||||
export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...'
|
||||
```
|
||||
|
||||
Now you have to start the Pylon service:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
With the access token, you can then do the following calls:
|
||||
|
||||
1. GraphQL:
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ info }'
|
||||
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name } }'
|
||||
curl -H "Authorization: Bearer $TOKEN" -G http://localhost:3000/graphql --data-urlencode 'query={ me { id name messages } }'
|
||||
|
||||
```
|
||||
|
||||
You can also visit the GraphQL playground at `http://localhost:3000/graphql` and execute the queries there.
|
||||
|
||||
2. Routes:
|
||||
|
||||
```
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/info
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me/messages
|
||||
```
|
||||
|
||||
## Completion
|
||||
|
||||
Congratulations! You have successfully integrated your Pylon with ZITADEL!
|
||||
|
||||
If you get stuck, consider checking out their [documentation](https://pylon.cronit.io). If you face issues, contact Pylon or raise an issue on [GitHub](https://github.com/getcronit/pylon/issues).
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: ZITADEL with Django Python
|
||||
sidebar_label: Django
|
||||
---
|
||||
|
||||
import AppJWT from '../imports/_app_jwt.mdx';
|
||||
import ServiceuserJWT from '../imports/_serviceuser_jwt.mdx';
|
||||
import ServiceuserRole from '../imports/_serviceuser_role.mdx';
|
||||
import SetupPython from '../imports/_setup_python.mdx';
|
||||
|
||||
This integration guide demonstrates the recommended way to incorporate ZITADEL into your Django Python application.
|
||||
It explains how to check the token validity in the API and how to check for permissions.
|
||||
|
||||
By the end of this guide, your application will have three different endpoint which are public, private(valid token) and private-scoped(valid token with specific role).
|
||||
|
||||
<Callout>
|
||||
This documentation references our [example](https://github.com/zitadel/example-django-python-oauth) on GitHub.
|
||||
</Callout>
|
||||
|
||||
## ZITADEL setup
|
||||
|
||||
Before we can start building our application, we have to do a few configuration steps in ZITADEL Management Console.
|
||||
|
||||
### Create application
|
||||
|
||||
<AppJWT components={props.components} />
|
||||
|
||||
### Create Serviceuser
|
||||
|
||||
<ServiceuserJWT components={props.components} />
|
||||
|
||||
### Give Serviceuser an authorization
|
||||
|
||||
<ServiceuserRole components={props.components} />
|
||||
|
||||
### Prerequisites
|
||||
|
||||
At the end you should have the following for the API:
|
||||
- Issuer, something like `https://example.zitadel.cloud` or `http://localhost:8080`
|
||||
- Introspection URL, something like `https://example.zitadel.cloud/oauth/v2/introspect`
|
||||
- Token URL, something like `https://example.zitadel.cloud/oauth/v2/token`
|
||||
- `.json`-key-file for the API, from the application
|
||||
- ID of the project
|
||||
|
||||
And the following from the Serviceuser:
|
||||
- `.json`-key-file from the serviceuser
|
||||
|
||||
## Setup new Django application
|
||||
|
||||
### Setup Python
|
||||
|
||||
<SetupPython components={props.components} />
|
||||
|
||||
### Install dependencies
|
||||
|
||||
For this example we need the following dependencies:
|
||||
- `django`: to create an API with django
|
||||
- `python-dotenv`: to use environment variables in the configuration
|
||||
- `authlib`: client-side OAuth functionality
|
||||
- `requests`: HTTP requests for the introspection
|
||||
|
||||
For the dependencies we need a requirements.txt-file with the following content:
|
||||
|
||||
```python reference
|
||||
https://github.com/zitadel/example-python-django-oauth/blob/main/requirements.txt
|
||||
```
|
||||
|
||||
Then install all dependencies with:
|
||||
```bash
|
||||
python -m pip install -U requirements.txt
|
||||
```
|
||||
|
||||
Then in your folder of choice, call the following command to create a Django base:
|
||||
```bash
|
||||
django-admin startproject myapi .
|
||||
```
|
||||
|
||||
## Define the Django API
|
||||
|
||||
### Add to the settings.py to include ZITADEL info
|
||||
|
||||
There is info needed for the introspection calls, which we put into the settings.py:
|
||||
|
||||
```python reference
|
||||
https://github.com/zitadel/example-python-django-oauth/blob/main/myapi/settings.py#L125-L133
|
||||
```
|
||||
|
||||
and create a ".env"-file in the root folder with the configuration as an example:
|
||||
```bash
|
||||
ZITADEL_INTROSPECTION_URL = 'URL to the introspection endpoint to verify the provided token'
|
||||
ZITADEL_DOMAIN = 'Domain used as audience in the token verification'
|
||||
API_PRIVATE_KEY_FILE_PATH = 'Path to the key.json created in ZITADEL'
|
||||
```
|
||||
|
||||
I should look something like this:
|
||||
|
||||
```bash
|
||||
ZITADEL_INTROSPECTION_URL = 'https://example.zitadel.cloud/oauth/v2/introspect'
|
||||
ZITADEL_DOMAIN = 'https://example.zitadel.cloud'
|
||||
API_PRIVATE_KEY_FILE_PATH = '/tmp/example/250719519163548112.json'
|
||||
```
|
||||
|
||||
### Validator definition
|
||||
|
||||
To validate the tokens, we need a validator which can be called in the event of API-calls.
|
||||
|
||||
validator.py:
|
||||
|
||||
```python reference
|
||||
https://github.com/zitadel/example-python-django-oauth/blob/main/myapi/validator.py
|
||||
```
|
||||
|
||||
### Requests and URLs
|
||||
|
||||
We define 3 different endpoints which differ in terms of requirements.
|
||||
views.py:
|
||||
|
||||
```python reference
|
||||
https://github.com/zitadel/example-python-django-oauth/blob/main/myapi/views.py
|
||||
```
|
||||
|
||||
To handle endpoints the urls have to be added to the urls.py:
|
||||
|
||||
```python reference
|
||||
https://github.com/zitadel/example-python-django-oauth/blob/main/myapi/urls.py
|
||||
```
|
||||
|
||||
### DB
|
||||
|
||||
Create and run migrations:
|
||||
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
You can use a local Django server to test the application.
|
||||
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
### Call the API
|
||||
|
||||
To call the API you need an access token, which is then verified by ZITADEL.
|
||||
Please follow [this guide here](/guides/integrate/token-introspection/private-key-jwt), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
|
||||
|
||||
Optionally set the token as an environment variable:
|
||||
```
|
||||
export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...'
|
||||
```
|
||||
|
||||
With the access token, you can then do the following calls:
|
||||
```
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8000/api/public
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8000/api/private
|
||||
curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:8000/api/private-scoped
|
||||
```
|
||||
|
||||
## Completion
|
||||
|
||||
Congratulations! You have successfully integrated your Django API with ZITADEL!
|
||||
|
||||
If you get stuck, consider checking out our [example](https://github.com/zitadel/example-python-django-oauth) application. This application includes all the functionalities mentioned in this quick-start. You can start by cloning the repository and defining the settings in the settings.py. If you face issues, contact us or raise an issue on [GitHub](https://github.com/zitadel/example-python-django-oauth/issues).
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user