diff --git a/docs/docs/examples/imports/_setup_pylon.mdx b/docs/docs/examples/imports/_setup_pylon.mdx new file mode 100644 index 0000000000..e68dad2c3c --- /dev/null +++ b/docs/docs/examples/imports/_setup_pylon.mdx @@ -0,0 +1 @@ +You have to install Pylon as described in [their documentation](https://pylon.cronit.io/docs/installation/). diff --git a/docs/docs/examples/secure-api/pylon.mdx b/docs/docs/examples/secure-api/pylon.mdx new file mode 100644 index 0000000000..a607d12cd8 --- /dev/null +++ b/docs/docs/examples/secure-api/pylon.mdx @@ -0,0 +1,304 @@ +--- +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"; +import SetupPylon from "../imports/_setup_pylon.mdx"; + +This integration guide demonstrates the recommended way to incorporate ZITADEL into your Pylon 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 Console. + +### Create application + + + +### Create Serviceuser + + + +### Give Serviceuser an authorization + + + +### 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 + +### Setup Pylon + + + +### Creating a new project + +To create a new Pylon project, run the following command: + +```bash +pylon new my-pylon-project +``` + +This will create a new directory called `my-pylon-project` with a basic Pylon project structure. + +### Project structure + +Pylon projects are structured as follows: + +``` +my-pylon-project/ +├── .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 +import { defineService } from "@getcronit/pylon"; + +export default defineService({ + Query: { + sum: (a: number, b: number) => a + b, + }, + Mutation: { + divide: (a: number, b: number) => a / b, + }, +}); +``` + +## 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 Console into the root folder of your project and rename it to `key.json`. + +### 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 { + defineService, + PylonAPI, + 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 default defineService({ + Query: { + me: User.me, + info: () => "Public Data", + }, + Mutation: { + createUser: User.create, + }, +}); + +export const configureApp: PylonAPI["configureApp"] = (app) => { + // 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("/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("/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("/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()); + }); +}; +``` + +### Call the API + +To call the API you need an access token, which is then verified by ZITADEL. +Please follow [this guide here](/docs/guides/integrate/token-introspection/private-key-jwt#get-an-access-token), ignoring the first step as we already have the `.json`-key-file from the serviceaccount. + +:::info +You can also create a PAT for the serviceuser and use it to test the API. For this, follow [this guide](/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat). +::: + +Optionally set the token as an environment variable: + +``` +export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...' +``` + +Now you have to start the Pylon service: + +```bash +bun run develop +``` + +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). diff --git a/docs/frameworks.json b/docs/frameworks.json index 0bf3e9d012..97afa11d47 100644 --- a/docs/frameworks.json +++ b/docs/frameworks.json @@ -111,5 +111,11 @@ "imgSrcDark": "/docs/img/tech/rustlight.svg", "docsLink": "https://github.com/smartive/zitadel-rust", "external": true + }, + { + "title": "Pylon", + "imgSrcDark": "/docs/img/tech/pylon.svg", + "docsLink": "https://github.com/getcronit/pylon", + "external": true } ] diff --git a/docs/sidebars.js b/docs/sidebars.js index 39f3d314d0..965a5d7a8c 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -38,6 +38,7 @@ module.exports = { "examples/secure-api/python-django", "examples/secure-api/python-flask", "examples/secure-api/nodejs-nestjs", + "examples/secure-api/pylon", { type: "link", label: ".Net", @@ -100,6 +101,11 @@ module.exports = { label: "Rust", href: "https://github.com/smartive/zitadel-rust", }, + { + type: "link", + label: "Pylon", + href: "https://github.com/getcronit/pylon", + }, ], }, { @@ -163,7 +169,7 @@ module.exports = { "guides/manage/customize/user-schema", ], }, - "guides/manage/terraform-provider" + "guides/manage/terraform-provider", ], }, { @@ -280,7 +286,7 @@ module.exports = { label: "Service Users", link: { type: "doc", - id: "guides/integrate/service-users/authenticate-service-users" + id: "guides/integrate/service-users/authenticate-service-users", }, collapsed: true, items: [ @@ -323,7 +329,10 @@ module.exports = { { type: "category", label: "Login users with SSO", - link: { type: "doc", id: "guides/integrate/identity-providers/introduction" }, + link: { + type: "doc", + id: "guides/integrate/identity-providers/introduction", + }, collapsed: true, items: [ "guides/integrate/identity-providers/google", @@ -349,7 +358,7 @@ module.exports = { label: "ZITADEL APIs", link: { type: "doc", - id: "guides/integrate/zitadel-apis/access-zitadel-apis" + id: "guides/integrate/zitadel-apis/access-zitadel-apis", }, collapsed: true, items: [ @@ -478,9 +487,8 @@ module.exports = { { type: "autogenerated", dirName: "concepts/structure", - } - ] - + }, + ], }, { type: "category", @@ -490,9 +498,8 @@ module.exports = { { type: "autogenerated", dirName: "concepts/features", - } - ] - + }, + ], }, { type: "autogenerated", @@ -807,10 +814,7 @@ module.exports = { type: "category", label: "Actions V2", collapsed: false, - items: [ - "apis/actionsv2/introduction", - "apis/actionsv2/execution-local", - ], + items: ["apis/actionsv2/introduction", "apis/actionsv2/execution-local"], }, { type: "doc", @@ -883,11 +887,9 @@ module.exports = { collapsed: false, link: { type: "doc", - id: "self-hosting/manage/cli/overview" + id: "self-hosting/manage/cli/overview", }, - items: [ - "self-hosting/manage/cli/mirror" - ], + items: ["self-hosting/manage/cli/mirror"], }, ], }, diff --git a/docs/static/img/tech/pylon.svg b/docs/static/img/tech/pylon.svg new file mode 100644 index 0000000000..e62fa6e281 --- /dev/null +++ b/docs/static/img/tech/pylon.svg @@ -0,0 +1 @@ + \ No newline at end of file