> For the complete documentation index, see [llms.txt](https://docs.bitsafe.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.bitsafe.finance/decentralization-manager/operations-and-reference/authentication-and-operator-roles.md).

# Authentication and operator roles

How operators sign in, which calls need the admin role, and which credentials a node uses toward Canton and wallets.

In this guide, a node is one running Decentralization Manager together with its Canton participant. This page lists each credential that a node accepts or presents, and the check that applies to it.

## Credentials at a glance

| Caller                                         | Path                                         | Credential                                            | Check                                                                       |
| ---------------------------------------------- | -------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------- |
| Operator, in the browser or with an API client | Operator API                                 | Access token from Keycloak or Auth0                   | Signature, issuer, and client ID on every request                           |
| Operator with the admin role                   | Admin-gated paths                            | The same access token, with the admin role            | Role check when `DECPM_ADMIN_ROLE` is set                                   |
| The node, toward the Canton Ledger API         | Ledger API calls for one Decentralized Party | Party credentials that the node stores for that party | Canton checks the rights of the Ledger API user                             |
| The node, toward the Canton Admin API          | Topology, package, and key calls             | None                                                  | Network access, plus optional TLS or mTLS                                   |
| A wallet                                       | Tenant API, `/v0/tenant/*`                   | Tenant API key                                        | Match against `DECPM_TENANT_API_KEYS`                                       |
| Another node                                   | Noise port                                   | Noise keys                                            | See [Connect peers](/decentralization-manager/get-started/connect-peers.md) |

## Sign in to the web interface and the operator API

The web interface uses the Authorization Code flow with PKCE. It needs a public client in your identity provider. The operator API uses the same access tokens.

Configure Keycloak or Auth0. Set the variables for one provider only. If you set both providers, the login page uses Auth0.

The login page reads `GET /auth-config` before the user has a token. An API client sends its access token in an `Authorization: Bearer <token>` header.

### Keycloak

In the Keycloak admin console, open **Clients → Create client** and use these settings:

| Setting                                                      | Value                                                       |
| ------------------------------------------------------------ | ----------------------------------------------------------- |
| Client type                                                  | OpenID Connect                                              |
| Client ID                                                    | Any value. Set the same value in `DECPM_KEYCLOAK_CLIENT_ID` |
| Client authentication                                        | Off (public client)                                         |
| Authentication flow                                          | **Standard flow** on, everything else off                   |
| Root URL and Home URL                                        | `https://<your-ui-host>`                                    |
| Valid redirect URIs                                          | `https://<your-ui-host>/*`                                  |
| Valid post-logout redirect URIs                              | `https://<your-ui-host>/*`                                  |
| Web origins                                                  | `https://<your-ui-host>`, or `+` to copy the redirect URIs  |
| Advanced → Proof Key for Code Exchange Code Challenge Method | `S256`                                                      |

Without **Web origins**, the browser blocks the token request with a CORS error. Login then fails.

Set `DECPM_KEYCLOAK_URL`, `DECPM_KEYCLOAK_REALM`, and `DECPM_KEYCLOAK_CLIENT_ID`.

Set `DECPM_KEYCLOAK_INTERNAL_URL` when the node cannot reach `DECPM_KEYCLOAK_URL`. The node then fetches the OIDC discovery document and the signing keys from the internal URL. The token issuer stays `DECPM_KEYCLOAK_URL`.

### Auth0

1. Open **Applications → APIs → Create API**. Use the `RS256` signing algorithm. The API **Identifier** is the audience.
2. Open **Applications → Applications → Create Application**, and pick **Single Page Application**.
3. Set **Allowed Callback URLs**, **Allowed Logout URLs**, and **Allowed Web Origins** to `https://<your-ui-host>`.
4. Set the token endpoint authentication method to **None**. Allow the **Authorization Code** and **Refresh Token** grant types.
5. In the application's **APIs** tab, authorize the application for the API from step 1.
6. Set `DECPM_AUTH0_DOMAIN` to the tenant domain, without a scheme.
7. Set `DECPM_AUTH0_CLIENT_ID` to the application's client ID, and `DECPM_AUTH0_AUDIENCE` to the API identifier.

Without **Allowed Web Origins**, the browser blocks the token request with a CORS error.

### How the node checks a token

The node checks every request to the operator API:

* It reads the `iss` claim and accepts only a trusted issuer.
* It verifies the token signature against the issuer's published keys. It keeps the keys for one hour.
* It rejects a token whose header names a different algorithm than the issuer's key.
* It requires the `azp` claim to equal the client ID of the matched configuration.
* It does not check the `aud` claim.

A missing or invalid token gets HTTP `401`.

The trusted issuers are the configured Keycloak or Auth0 provider, plus the provider of every stored [party credential](#party-credentials). For a party credential's provider, the `azp` claim must equal that credential's client ID.

{% hint style="warning" %}
A token that a party credential's client mints passes the sign-in check. Protect each party client secret like an operator credential.
{% endhint %}

The node allows same-origin browser requests only. Set `DECPM_ALLOWED_ORIGIN` when the web interface loads from a different origin than the API.

### Paths without sign-in

| Path                                                            | Purpose                                                                        |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `/`, `/index.html`, `/assets/*`, `/favicon.ico`, `/favicon.svg` | Web interface files                                                            |
| `/auth-config`                                                  | Provider settings for the login page                                           |
| `/healthz`                                                      | Liveness probe                                                                 |
| `/.well-known/*`                                                | Reserved probe paths                                                           |
| `/swagger-ui/*`, `/api-docs/*`                                  | Swagger UI. The node mounts it only in [insecure mode](#insecure-mode)         |
| `/v0/tenant/*`                                                  | Tenant API. Each handler checks the [tenant API key](#tenant-api-keys) instead |
| First `PUT /party-config` on a new node                         | See [First-run bootstrap](#first-run-bootstrap)                                |

Every other path on the HTTP port needs a valid token.

The metrics listener on `DECPM_METRICS_PORT` (default `9464`) is a separate listener with no authentication. It binds to the same address as the HTTP port, `DECPM_HOST`. Keep that port on a private network. See [Monitoring and logging](/decentralization-manager/operations-and-reference/monitoring-and-logging.md).

## The admin role

`DECPM_ADMIN_ROLE` names the role that admin-gated paths require. When the variable is unset, every signed-in caller passes the admin check. The node then logs a warning at startup.

Set `DECPM_ADMIN_ROLE` on every node that more than one person can sign in to.

A signed-in caller without the role gets HTTP `403`. The role name must match exactly, including case.

### Where the node reads roles

| Claim                                       | Typical source                                                  |
| ------------------------------------------- | --------------------------------------------------------------- |
| `realm_access.roles`                        | Keycloak realm roles                                            |
| `roles`                                     | A flat array of role names                                      |
| `scope`                                     | Space-separated scopes                                          |
| The claim that `DECPM_JWT_ROLE_CLAIM` names | A namespaced custom claim that holds an array of strings        |
| `permissions`                               | Auth0 RBAC. The node reads this claim only from an Auth0 issuer |

For Keycloak, create a realm role with the exact name in `DECPM_ADMIN_ROLE`. Assign the role to the admin users.

### Admin role with Auth0

Auth0 does not put roles in access tokens by default. Create the role under **User Management → Roles** and assign it to the admin users. Then put the role in the token in one of these three ways.

**Option 1: RBAC permissions (recommended).** On the API, turn on **RBAC** and **Add Permissions in the Access Token**. Add a permission with the role name, and grant the permission to the role. Auth0 then sends a `permissions` array, and no other setting is necessary.

**Option 2: RBAC scope.** Use the same API settings, and set `DECPM_AUTH0_SCOPE` to the role name. The web interface then requests that scope in addition to `openid profile email`. Auth0 returns a permission in `scope` only when the client asks for it.

**Option 3: namespaced custom claim.** Add a Login flow Action under **Actions → Library → Build Custom**:

```js
exports.onExecutePostLogin = async (event, api) => {
  const roles = event.authorization?.roles || [];
  api.accessToken.setCustomClaim("https://example.com/decman/roles", roles);
};
```

Then set `DECPM_JWT_ROLE_CLAIM=https://example.com/decman/roles`. Use a namespace that your organization controls. Auth0 drops a custom claim that is not URI-namespaced.

When an admin-gated call returns `403`, decode the access token. Look for the role in `permissions`, `scope`, or your `DECPM_JWT_ROLE_CLAIM` claim. A token with only `openid profile email` carries no role.

### Paths that need the admin role

| Area             | Paths                                                                                                                                                                                                                                                                                  |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Configuration    | `PUT /party-config` after the first party credential exists, `POST /party-config/discover-member-party`, `POST /network-config`, `POST /auth/grant-rights`                                                                                                                             |
| Packages         | `POST /dars/upload`, which uploads DARs to this participant only                                                                                                                                                                                                                       |
| Workflow starts  | `POST /onboarding`, `POST /add-party`, `POST /kick`, `POST /change-threshold`, `POST /contracts`, `POST /dars/distribute`, `POST /dars/distribute-package`                                                                                                                             |
| Workflow control | `POST /onboarding/cancel`, `POST /add-party/cancel`, `POST /kick/cancel`, `POST /change-threshold/cancel`, `POST /contracts/cancel`, `POST /dars/cancel`, `POST /workflows/{instance_name}/cancel`, `POST /workflows/{instance_name}/retry`, `POST /workflows/{instance_name}/dismiss` |
| Invitations      | `POST /invitations/accept`, `POST /invitations/decline`, `GET /invitations/{id}/dars/{index}`                                                                                                                                                                                          |
| Governance       | `POST /governance/propose`, `POST /governance/confirm`, `POST /governance/execute`, `POST /governance/expire`, `POST /governance/cancel-proposal`                                                                                                                                      |
| Token standard   | `POST /token-standard-contracts`                                                                                                                                                                                                                                                       |

`GET /governance/confirmations` answers every signed-in caller. It returns the `proposal_payload` and `proposal_template_id` fields only to a caller with the admin role.

### Paths that any signed-in caller can use

Every other operator API path needs a valid token, but not the admin role. These paths include every read path except `GET /invitations/{id}/dars/{index}`, for every party on the node. They also include three write paths:

| Path                            | Effect                                                                                                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /auth/test`               | Gets a fresh Canton token for each stored party credential                                                                                                 |
| `POST /governance/cancel`       | Revokes this node's own confirmation of a governance action                                                                                                |
| `DELETE /acs-import-quarantine` | Lifts an ACS-import quarantine. See [Add a member to a Decentralized Party](/decentralization-manager/operations-and-reference/add-a-member.md#quarantine) |

{% hint style="warning" %}
The admin role does not protect these three calls. On a shared node, give sign-in access only to people that you trust with them.
{% endhint %}

The web interface build flag `VITE_ADMIN_ACCESS` hides or disables some admin controls. This flag is not a security control. The node checks the admin role on every request.

## Party credentials

A node calls the Canton Ledger API for a Decentralized Party with the party credentials that it stores for that party. Each node stores its own credentials for each party that it hosts.

A party credential holds these values:

* The Decentralized Party ID.
* The member party ID. This party is local to the participant, and it acts as this node's member of the Decentralized Party.
* The Ledger API user ID.
* Client credentials at Keycloak or Auth0.

| Provider | Request fields                                                                                   | Grant                                                                                     |
| -------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| Keycloak | `keycloak_url`, `keycloak_realm`, `keycloak_client_id`, `keycloak_client_secret`                 | Client credentials                                                                        |
| Keycloak | `keycloak_url`, `keycloak_realm`, `keycloak_client_id`, `keycloak_username`, `keycloak_password` | Password grant. Only the API accepts it. The web interface offers client credentials only |
| Auth0    | `auth0_domain`, `auth0_audience`, `auth0_client_id`, `auth0_client_secret`                       | Machine-to-machine client credentials                                                     |

Use a confidential client for each party. Do not reuse the public sign-in client.

### Store credentials in the web interface

1. Open **Parties** and click the party.
2. Under **Authentication**, click **Login**.
3. Enter the provider fields.
4. Click **Discover Member Party**. The node gets a token and reads the user's primary party from Canton. To type the values yourself, click **Enter manually**.
5. Check **Member Party ID** and **User ID**, then click **Save**.
6. Click **Test Auth**.
7. If rights are missing, click **Grant Rights**. See [Canton rights](#canton-rights).

To change the stored values, click **Edit Credentials**.

### Store credentials with the API

```bash
curl --fail-with-body -X PUT "$DECPM_URL/party-config" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "dec_party_id": "governed-app::1220...",
    "member_party_id": "member-1::1220...",
    "user_id": "member-1-ledger-user",
    "keycloak_url": "https://keycloak.example.com",
    "keycloak_realm": "my-realm",
    "keycloak_client_id": "member-1-ledger-client",
    "keycloak_client_secret": "<client-secret>"
  }'
```

For Auth0, send `auth0_domain`, `auth0_audience`, `auth0_client_id`, and `auth0_client_secret` instead of the `keycloak_*` fields.

The node applies these rules:

* For Keycloak, a secret field that you leave out keeps the stored value. An empty string clears it.
* A changed `keycloak_url` or `auth0_domain` needs a fresh secret in the same request. The node never sends a stored secret to a new address.
* The node answers HTTP `400` to a request with an unknown field. This rule includes the old `packages` field.
* `GET /party-config/{dec_party_id}` returns the stored values with the secrets masked.

### Canton rights

The Ledger API user needs four rights:

| Right    | Party               |
| -------- | ------------------- |
| `actAs`  | Member party        |
| `readAs` | Member party        |
| `actAs`  | Decentralized Party |
| `readAs` | Decentralized Party |

**Grant Rights** grants exactly these four rights, and nothing wider. It asks for client credentials whose identity holds `ParticipantAdmin` on this participant. The node uses those credentials once and does not store them.

The API call is `POST /auth/grant-rights` with `dec_party_id`, `admin_client_id`, and `admin_client_secret`.

The node checks and grants the rights for the user in the token's `sub` claim. It uses the stored user ID only when the token has no `sub` claim. `GET /auth/status` reports the token state and the rights for each party.

### Encrypt stored secrets

The node stores party credentials in its SQLite database, `decpm.db`. Set `DECPM_DB_ENCRYPTION_KEY` to a random passphrase. The node then encrypts the client IDs, secrets, user names, and passwords with AES-256-GCM. The key is the SHA-256 hash of the passphrase.

Without `DECPM_DB_ENCRYPTION_KEY`, the database holds these secrets in plaintext. Keep the passphrase stable. A node with a different passphrase cannot read the stored secrets.

### First-run bootstrap

A node with no stored party credential accepts the first `PUT /party-config` without a token. The node runs one such request at a time. It rejects a concurrent bootstrap request with HTTP `409`.

After the first party credential exists, `PUT /party-config` needs a valid token. It also needs the admin role when the node sets `DECPM_ADMIN_ROLE`.

The first party credential also adds its identity provider to the trusted issuers. So the first caller chooses an identity provider that the node then trusts for sign-in.

A node with no top-level provider and no party credential rejects every token. On such a node, only the paths without sign-in and this bootstrap call work.

{% hint style="danger" %}
**Hardening.** Keep the HTTP port of a new node unreachable from untrusted networks. Store the first party credential yourself, right after the first start. Then check the stored values with `GET /party-config/{dec_party_id}`.
{% endhint %}

## Tenant API keys

The tenant API, `/v0/tenant/*`, creates external parties and adds hosts to external and local parties. Wallets and operator scripts call it. It does not use the sign-in token or the admin role. Each call sends a tenant API key in an `Authorization: Bearer <key>` header.

* Set `DECPM_TENANT_API_KEYS` to one or more random keys, separated by commas. The node reads the keys at startup.
* With no keys, the node answers every tenant call with HTTP `401`.
* The node compares keys in constant time.
* A key is not bound to a party. Any valid key can call every tenant path for every party on the node.
* Each node accepts only its own keys. A wallet keeps one key for each host.

Set keys only on a node that hosts external parties, or that adds hosts to a local party. Keep the keys in a secret store. Route the tenant API to the wallet over TLS.

To test a key, ask for a party that does not exist. The right key gets `404`, and a wrong key gets `401`:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer $TENANT_KEY" \
  "$DECPM_URL/v0/tenant/test::1220$(printf '0%.0s' {1..64})/state"
```

To rotate a key, add the new key and restart the node. Move the wallet to the new key. Then remove the old key and restart the node again.

The procedures are in [Decentralize an external party](/decentralization-manager/operations-and-reference/decentralize-an-external-party.md) and [Decentralize an existing party](/decentralization-manager/operations-and-reference/decentralize-an-existing-party.md).

## Insecure mode

`--insecure`, or `DECPM_INSECURE=true`, runs a node without an identity provider. Use it only for local development against a Canton participant with unsafe shared-secret authentication.

In insecure mode, the node does these things:

* It accepts any inbound token, or no token, and gives every caller the admin role.
* It answers `/auth-config` with `auth_required: false`, so the web interface shows no login.
* It presents an HS256 token to Canton. `DECPM_CANTON_HMAC_SECRET` signs it (default `unsafe`).
* It sets the token's `aud` from `DECPM_CANTON_HMAC_AUDIENCE` (default `https://canton.network.global`). It sets `sub` from `DECPM_CANTON_HMAC_SUBJECT` (default `ledger-api-user`).
* It accepts tenant API calls without a key.
* It serves the Swagger UI at `/swagger-ui/`.

The node refuses to start in insecure mode unless the Canton network is `devnet`. `DECPM_CANTON_NETWORK` sets the network, and its default is `devnet`. The node logs a warning at startup whenever insecure mode is on.

```bash
dec-party-manager -d ./development/participant-1 serve --insecure
```

## Hardening checklist

* Leave insecure mode off on every shared node.
* Set `DECPM_ADMIN_ROLE`, and assign the role only to operators.
* Set `DECPM_DB_ENCRYPTION_KEY`, and keep the passphrase in a secret store.
* Store the first party credential before the HTTP port is reachable from untrusted networks.
* Keep the metrics port on a private network.
* Keep the Canton Admin API on a private network, with TLS or mTLS where it crosses hosts. The node calls the Admin API without a token.
* Set tenant API keys only where a wallet needs them.
* Protect each party client secret like an operator credential.

The [Configuration reference](/decentralization-manager/operations-and-reference/configuration-reference.md) lists every variable on this page.

## Technical sources

* [Inbound auth middleware](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/server/middleware/auth.rs)
* [JWT validator](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/auth/validators/jwt.rs)
* [Role and issuer helpers](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/auth/validators/common.rs)
* [Party credential handlers](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/server/handlers/party_config.rs)
* [Auth status and grant-rights handlers](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/server/handlers/auth.rs)
* [Server setup and insecure-mode guard](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/server/mod.rs)
* [CLI flags and environment variables](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/cli.rs)
* [Secret encryption](https://github.com/DLC-link/decentralization-manager/blob/main/crates/decman/src/db/crypto.rs)
* [Deployment guide](https://github.com/DLC-link/decentralization-manager/blob/main/docs/DEPLOYMENT_GUIDE.md)
* [README](https://github.com/DLC-link/decentralization-manager/blob/main/README.md)

## Related pages

* [Production deployment](/decentralization-manager/get-started/production-deployment.md)
* [Configuration reference](/decentralization-manager/operations-and-reference/configuration-reference.md)
* [Security architecture](/decentralization-manager/operations-and-reference/security-architecture.md)
* [API and workflow reference](/decentralization-manager/operations-and-reference/api-and-workflow-reference.md)

## Next step

👉 **Distribute the packages that your parties use:** [Package and contract operations](/decentralization-manager/operations-and-reference/package-and-contract-operations.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.bitsafe.finance/decentralization-manager/operations-and-reference/authentication-and-operator-roles.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
