Skip to content

Provisioning

The Keycloak authorization configuration is provisioned by the MigrationService - a background service that runs on application startup. It uses the Keycloak Admin REST API to create and update the realm, clients, roles, and authorization settings.

The MigrationService is the single source of truth for all Keycloak configuration. There is no manual setup required in the Keycloak admin console.

Migration Workflow

The KeycloakMigrator orchestrates seven steps:

StepAction
1. Health checkWait for Keycloak to be ready
2. RealmEnsure the voltimax realm exists
3. ClientsCreate or update OIDC clients
4. Client UUIDResolve the API client's internal UUID
5. RolesCreate client roles on the API client
6. AuthorizationImport resources, scopes, policies, permissions
7. Dev userCreate a dev user with admin role (development only)

Steps are idempotent - running the migration multiple times produces the same result. This makes it safe to run on every deployment.

Realm Configuration

All configuration is defined in KeycloakRealmConfiguration, a static class that serves as the central definition for:

  • Clients: OIDC client registrations (iot-platform-api, iot-platform-scalar, iot-frontend, iot-platform-mcp)
  • Roles: Client roles on the API client (platform-admin, platform-operator, platform-viewer, gateway-device)
  • Authorization settings: Resources, scopes, policies, and permissions as a ResourceServerRepresentation

Clients

Four OIDC clients are registered on the voltimax realm: a confidential resource server for the Platform API (with Authorization Services enabled), and public PKCE clients for the Scalar docs UI, Vue frontend, and MCP server. Client definitions are in KeycloakRealmConfiguration.GetClients().

Protocol mappers

Public clients include two protocol mappers:

  • realm-roles: Maps realm roles into the roles claim in access and ID tokens
  • audience: Adds the API client as the aud (audience) claim for token validation

Authorization Import

The most critical provisioning step is the authorization import (Step 6). This uses the KeycloakAuthorizationManager to call Keycloak's bulk import endpoint:

http
POST /admin/realms/{realm}/clients/{uuid}/authz/resource-server/import

The request body is a ResourceServerRepresentation containing the full authorization configuration: resources, scopes, policies, and permissions. This is built by KeycloakRealmConfiguration.GetAuthorizationSettings().

Why Bulk Import?

The import endpoint was chosen over individual CRUD operations for several reasons:

  • Atomicity: All resources, scopes, policies, and permissions are created in a single operation. No partial states.
  • Idempotency: Running the import again replaces the existing configuration rather than creating duplicates.
  • Cross-references: Permissions reference both resources and policies by name. The import endpoint resolves these references automatically.
  • Data model alignment: The ResourceServerRepresentation matches the Keycloak export format, making it easy to verify the configuration in the admin console.

KeycloakAuthorizationManager

The manager is a thin wrapper around the Kiota-generated Admin API client. It takes the realm name and the internal UUID of the API client (not the clientId string, which is resolved in Step 4) and calls the import endpoint.

Resource Definitions

Resources are created using the CreateResource helper:

csharp
CreateResource("gateways", "urn:voltimax:gateways", ["read", "write", "delete"])

Each resource specifies:

  • Name: Matches what the API uses in RequireProtectedResource(name, scope)
  • Type: A URN-style identifier for categorization
  • Scopes: The actions available on this resource

Policy Definitions

Role Policies

Role policies are type "role" and reference a client role:

csharp
CreateRolePolicy(
    "platform-admin-policy",
    "Grants access to users with platform-admin role",
    "platform-admin")

The role reference is stored as a JSON array in the policy's config:

json
[{ "id": "iot-platform-api/platform-admin", "required": true }]

Scope Permissions

Scope permissions are type "scope" and bind a resource + scope to policies:

csharp
CreateScopePermission(
    "gateways-read",         // permission name
    "gateways",              // resource
    "read",                  // scope
    ["platform-admin-policy", "platform-operator-policy",
     "platform-viewer-policy", "gateway-device-policy"])

The decision strategy for scope permissions is AFFIRMATIVE - if any of the listed policies grants access, the permission is satisfied.

Aspire Integration

The .NET Aspire AppHost wires Keycloak connection settings to both the MigrationService and Platform API via environment variables. The MigrationService connects to the master realm with admin credentials to perform provisioning. The Platform API connects to the voltimax realm as the iot-platform-api client for authorization evaluation.

In local development, Keycloak runs as a container with a dedicated PostgreSQL instance. The client secret is auto-generated by Aspire and shared between the MigrationService (which sets it on the client) and the Platform API (which uses it for authorization requests).

Adding a New Resource

To add a new protected resource to the authorization model:

  1. Add the resource in GetAuthorizationResources():

    csharp
    CreateResource("my-resource", "urn:voltimax:my-resource", ["read", "write"])
  2. Add scope permissions in GetAuthorizationPolicies():

    csharp
    CreateScopePermission("my-resource-read", "my-resource", "read",
        ["platform-admin-policy", "platform-operator-policy", "platform-viewer-policy"]),
    CreateScopePermission("my-resource-write", "my-resource", "write",
        ["platform-admin-policy", "platform-operator-policy"])
  3. Deploy - the MigrationService will import the updated configuration on next startup

  4. Match in API - add corresponding Permission constants and apply to endpoints. See API Protection.