Skip to main content

ZITADEL with Pylon

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​

  1. Go to your Project and click on the New button as shown below.

Register the API

  1. Give a name to your application (Test API is the name given below) and select type API.

Register the API

  1. Select JWT as the authentication method and click Continue.

Register the API

  1. Now review your configuration and click Create.

Register the API

  1. You will now see the API’s Client ID. You will not see a Client Secret because we are using a private JWT key.

Register the API

  1. Next, we must create the key pairs. Click on New.

Register the API

  1. Select JSON as the type of key. You can also set an expiration time for the key or leave it empty. Click on Add.

Register the API

  1. Download the created key by clicking the Download button and then click Close.

Register the API

  1. The key will be downloaded.

Register the API

  1. When you click on URLs on the left, you will see the relevant OIDC URLs. Note down the issuer URL, token_endpoint and introspection_endpoint.

Register the API

  1. The key that you downloaded will be of the following format.
{
"type":"application",
"keyId":"<YOUR_KEY_ID>",
"key":"-----BEGIN RSA PRIVATE KEY-----\<YOUR_PRIVATE_KEY>\n-----END RSA PRIVATE KEY-----\n",
"appId":"<YOUR_APP_ID>",
"clientId":"<YOUR_CLIENT_ID>"
}
  1. Also note down the Resource ID of your project.

Register the API

Create Serviceuser​

  1. Go to the Users tab in your organization as shown below and click on the Service Users tab.

Register the API

  1. To add a service user, click on the New button.

Register the API

  1. Next, add the details of the service user and select either Bearer or JWT for Access Token Type and click on Create. For this example, we will select JWT.

Register the API

  1. Now you will see the saved details.

Register the API

  1. Next, we need to generate a private-public key pair in ZITADEL and you must get the private key to sign your JWT. Go to Keys and click on New.

Register the API

  1. Select type JSON and click Add.

Register the API

  1. Download the key by clicking Download. After the download, click Close.

Register the API

  1. You will see the following screen afterwards.

Register the API

  1. The downloaded key will be of the following format:
{
"type":"serviceaccount",
"keyId":"<YOUR_KEY_ID>",
"key":"-----BEGIN RSA PRIVATE KEY-----\n<YOUR_KEY>\n-----END RSA PRIVATE KEY-----\n",
"userId":"<YOUR_USER_ID>"
}

Give Serviceuser an authorization​

In order to access this route, you must create the role read:messages in your ZITADEL project and also create an authorization for the service user you created by adding the role to the user. Follow these steps to do so:

  1. Go to your project and select Roles. Click New.

Register the API

  1. Add the read:messages role as shown below and click Save.

Register the API

  1. You will see the created role listed.

Register the API

  1. To assign this role to a user, click on Authorizations.

Register the API

  1. Select the user you want to assign the role to.

Register the API

  1. Select the project where this authorization is applicable.

Register the API 7. Click Continue.

Register the API

  1. Select the role read:messages and click Save.

Register the API

  1. You will now see the your service user has been assigned the role read:messages.

Register the API

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​

You have to install Pylon as described in their documentation.

Creating a new project​

To create a new Pylon project, run the following command:

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:

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:
AUTH_ISSUER='URL to the zitadel instance'
AUTH_PROJECT_ID='ID of the project'

It should look something like this:

AUTH_ISSUER='https://example.zitadel.cloud'
AUTH_PROJECT_ID='250719519163548112'
  1. 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:

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, 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.

Optionally set the token as an environment variable:

export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...'

Now you have to start the Pylon service:

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.

  1. 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. If you face issues, contact Pylon or raise an issue on GitHub.