IAM (Zitadel) Initial Setup on New Cluster — Unique AI Documentation

IAM (Zitadel) Initial Setup on New Cluster

This is a guide on how to configure and manage Unique’s Identity Access Management (IAM) solution Zitadel for use with the Unique platform. Refer to the applicable section for what you want to configure.

When setting up a new organization, follow all the steps in this guide in order to have a working setup.


1. Cluster IAM organization

On a new environment or local setup there is a root organization in Zitadel called “Cluster IAM”. Only one root user exists and needs to be used to login for the first time to perform the initial setup.

Where to find the root user

The root user’s password must be changed when initially logging in.

The Cluster IAM organization is used to manage the Zitadel Project that defines the user roles for the entire environment. The project (and its roles) are then granted to other organizations in the same environment.

After successfully logging in with the root user and changing the password, the root user’s profile page in Zitadel will be displayed. Click on the “Default settings” button on the top right to get to the instance overview of Zitadel.

2. Creating an organization

On the instance overview screen, navigate to the “Organizations” section and click on the “+ New” button to create a new organization. Enter the organization name and click “Create”. You will be redirected to the overview that now includes the newly created organization.

After creating a new organization, a project can be setup and granted to it.

3. Setup a project

Navigate back to the Cluster IAM organization overview by clicking on the “Organization” button on the top right.

Open the “Projects” tab and make sure you’re on the “Cluster IAM” root organization. Then click on “Create New Project” and enter a name for the project (e.g.: “unique”).

3.0.1 Project Settings

Once you have created the project, make sure you have these settings checked.

3.1 Setup an application

Once you’ve created a project in the root organization an application needs to be setup and configured inside that project. Click on the tile with the “+” and enter an application name (e.g.: “unique-app”). For the type of the application, choose “WEB”.

In the second step, choose the Authentication Method “PKCE”.

In the third step, you need to enter the Redirect URIs and Post Logout URIs. Here’s a list of the URIs you need to add. They are the same for Redirect and Post Logout, but listed independently here for completeness. Replace “” with the base URL of your environment.

Redirect URIs

- https://<baseUrl>/chat
- https://<baseUrl>/knowledge-upload
- https://<baseUrl>/theme
- https://<baseUrl>/admin

Post Logout URIs

- https://<baseUrl>/chat
- https://<baseUrl>/knowledge-upload
- https://<baseUrl>/theme
- https://<baseUrl>/admin

Development Mode

If working with a local setup for development, the Development Mode needs to be enabled to allow for http localhost URLs of the services (same for Redirect and Post Logout). The URLs for local development the following:

- http://localhost:3003/chat
- http://localhost:3004/knowledge-upload
- http://localhost:3005/theme
- http://localhost:3006/admin

On the last step, review and confirm the configuration for the application and click on “Create”.

3.2 Configure application token settings

After the application has been created, navigate to the “Token Settings” section on the application overview page. Ensure the following settings are configured and click on “Save”.

3.3 Configure project roles

Roles are granted as authorizations to users in Zitadel to give access to certain features of Unique solution.

All the roles and their descriptions can be found on the following page: Understand Roles and Permissions

Navigate to the “Roles” section on the project click on “+ New” and add all roles defined on the “Roles and Permissions” page linked above.

The screenshot might be up to date and is not showing all roles that currently exist. It only serves as an example of roles added in a project. Refer to Understand Roles and Permissions and add all roles that are listed there to make sure you are up to date.

3.4 Grant project to an organization

The project configured in the root “Cluster IAM” organization needs to be granted to other organizations on the Zitadel instance. To do this, navigate to the “Grants” section on the project in the “Unique IAM” root organization and click on the “+ New” button.

Search for the organization you want to add the grant for and continue. Select all the roles and save the grant.

Creating additional (new) roles in Zitadel

When adding new roles, the following actions are required:

  1. Add the new role to the project on the “Cluster IAM” root organization (as described in the previous section).
  2. Add the new role to the Grant given to the organizations. This can be done by clicking on a grant and editing it to make sure the new role is included in the grant.

4. Set-up Service user

4.1 Scope Management Service User

The Unique application needs a service user for syncing the user data we have in Zitadel with our Unique System. To set this service user up, the following steps are necessary:

  1. In Zitadel, set-up the scope-management service user on the organisation where the Unique project has been created (Cluster IAM normally), following this documentation How To Configure A Service User. This service user needs no Unique roles to function.

  2. In Zitadel, generate a Personal Access Token (PAT) for the created service user, details to be found here: How To Configure A Service User. Copy the PAT after creation, you will need it in step 4 to store it in the Azure Key Vault.

  3. In Zitadel, give the scope-management service user, the Iam Owner role on an instance level. To switch to the instance, simply click on Default Setting at the top right in Zitadel:

  4. Then add the Service user and give it the role under this button in Zitadel:

  5. After the role was assigned to the user, it should show up like this in the list of the users with instance roles:

  6. In the Azure Key Vault, search for the key vault that contains the secret manual-zitadel-scope-mgmt-pat and add the generated PAT from step 2 there as a value.

After setting the PAT in the Key Vault it is necessary to redeploy, so that the scope-management and the user-sync job can pull the new secret from the Key Vault.

After performing the setup of the scope-management service user, the user-sync cronjob is able to use this service user user’s PAT from the key vault to make requests to the Zitadel API and sync the provisioned users to the Unique backend.

5. Adding Zitadel Actions

ZITADEL Actions are custom scripts that you can define to run at specific points during the authentication and authorization processes. After an action was added, it needs to be used by assigning it to one of the available Trigger Types.

Currently the following Actions need to be configured (as required).

Action: addGrant

  1. Create a new addGrant action

    Note: add the appropriate roles based on needs

function addGrant(ctx, api) {
    api.userGrants.push({
        projectID: '<Resource ID of granted project Unique Apps>',
        projectGrantID: '<Grant ID of granted project Unique Apps>',
        roles: ['chat.chat.basic'] // default roles users get
    });
}

The addGrant actions needs to react on the Trigger Type “Post Creation” because we want to add the default roles for the user after creation.

This action will be triggered every time a user is created.

Action: addUserGroupsMetadataEntra

  1. If Entra is used as an IDP, create the action addUserGroupsMetadataEntra.
const logger = require("zitadel/log")
const http = require('zitadel/http')

function addUserGroupsMetadataEntra(ctx, api) {
  logger.log('Writing group info on user metadata.');

const claims = ctx.claimsJSON();
  const parsedGroups = JSON.parse(claims).groups;

// Get names with additional call to Graph API
  const batchRequests = [];
  for (const groupId of parsedGroups) {
    batchRequests.push({
      "id": groupId,
      "method": "GET",
      "url": `/groups/${groupId}`
    });
  }

const graphApiBatchResponse = http.fetch('https://graph.microsoft.com/v1.0/$batch', {
    method: 'POST',
    headers: {
      "Authorization": `Bearer ${ctx.accessToken}`,
      "Content-Type": "application/json"
    },
    body: {
      requests: batchRequests
    }
  }).json();

const entraGroups = [];
  for (const apiResponse of graphApiBatchResponse.responses) {
    const groupId = apiResponse.id;
    const groupName = apiResponse.body.displayName;

entraGroups.push({
      "id": groupId,
      "displayName": groupName,
    });
  }

// Check if groups are valid and do not contain empty string values
  if (entraGroups.some(group => group.id === "" || group.displayName === "")) {
    logger.log('Invalid groups found, skipping metadata update.');
    return;
  }

// append metadata to user
  api.v1.user.appendMetadata("groups", entraGroups);
}

After you’ve added the action, you need to assign it to a trigger.

Action: addUserGroupsMetadataOidc

const logger = require("zitadel/log")

function addUserGroupsMetadataOidc(ctx, api) {
  logger.log('Writing group info on user metadata.');

const claims = ctx.claimsJSON();
  const parsedGroups = JSON.parse(claims).groups || [];

let groups = parsedGroups.map(group => {
    return {
      "id": group.id,
      "displayName": group.displayName,
    };
  });

// Check if groups are valid and do not contain empty string values
  if (groups.some(group => group.id === "" || group.displayName === "")) {
    logger.log('Invalid groups found, skipping metadata update.');
    return;
  }

// append metadata to user
  api.v1.user.appendMetadata("groups", groups);
}

Flow Type: External Authentication
Trigger Type: Post Authentication
Actions: addUserGroupsMetadataOidc

Action: addMicrosoftUserDataEntra

const http = require('zitadel/http')

function addMicrosoftUserDataEntra(ctx, api) {
  let country;
  let department;
  const profileApiResponse = http.fetch('https://graph.microsoft.com/v1.0/me?$select=country,department', {
    method: 'GET',
    headers: {
      "Authorization": `Bearer ${ctx.accessToken}`,
      "Content-Type": "application/json"
    }
  }).json();

country = profileApiResponse.country || null;
  department = profileApiResponse.department || null;

api.v1.user.appendMetadata("country", country);
  api.v1.user.appendMetadata("department", department);
}

More fields can be fetched from the Microsoft Graph API: More Properties.

Flow Type: External Authentication
Trigger Type: Post Authentication

Action: addUserGroupsMetadataSaml

const logger = require("zitadel/log")

function addUserGroupsMetadataSaml(ctx, api) {
  logger.log('Writing group info on user metadata.');

const sentGroups = ctx.v1.providerInfo?.attributes["groups"];

let groups = sentGroups.map(group => {
    return {
      "id": group.id,
      "displayName": group.displayName,
    };
  });

// append metadata to user
  api.v1.user.appendMetadata("groups", groups);
}

Flow Type: External Authentication
Trigger Type: Post Authentication

Action: prefilRegisterFromSAML

function prefilRegisterFromSAML(ctx, api) {
    if (ctx.v1.externalUser?.externalIdpId != "xxxxxxx") {
        return;
    }
    let firstname = ctx.v1.providerInfo?.attributes["GivenName"];
    let lastname = ctx.v1.providerInfo?.attributes["Surname"];
    let email = ctx.v1.providerInfo?.attributes["emailaddress"];

if (firstname) {
     api.setFirstName(firstname[0]);
    }
    if (lastname) {
       api.setLastName(lastname[0]);
    }
    if (email) {
     api.setEmail(email[0]);
     api.setEmailVerified(true);
     api.setPreferredUsername(email[0]);
    }
}

Flow Type: External Authentication
Trigger Type: Post Authentication

Don’t forget to add the IDP ID in the code above. You will find it in the IDP settings when clicking on the IDP and getting the ID from the URL.

6. Configuring SSO

For SSO configuration refer to the corresponding documentation on this page here: How To Setup Single Sign-On (SSO)