We will use a self-signed certificate and a single-node Kubernetes cluster for demonstration purposes. In production, you would likely use a certificate signed by a trusted certificate authority (CA) and a multi-node cluster.
The users/groups will be accessed via the OpenID Connect (OIDC) protocol and WildFly's native OIDC support implementation. Entra ID groups will be mapped to WildFly roles, which will be used to control access to Orbeon Forms.
We will create two groups/roles:
orbeon-user
orbeon-admin
As well as two test users:
testuser1 (member of orbeon-user)
testuser2 (member of orbeon-user and orbeon-admin)
Orbeon Forms will be accessible only to users from the orbeon-user group (i.e. to both testuser1 and testuser2). Form Builder and Forms Admin pages will be accessible only to users from the orbeon-admin group (i.e. to testuser2 only).
A more complete example Bash script is available on GitHub. It includes more commands, which will check if the resources already exist, among other things, but it follows roughly the same steps as described here.
Some values in the commands used in this guide will be hardcoded for simplicity. In a real-world scenario, you would likely want to parameterize them, for example by using environment variables.
Requirements
The main requirement is an account with an Azure subscription.
The following utilities will be used during the installation process:
az (Azure CLI)
psql (PostgreSQL client)
kubectl (Kubernetes deployment)
jq (JSON manipulation)
base64 (Kubernetes passwords encoding)
core Linux utilities such as cat, curl, echo, etc.
All steps described below can also be done manually via the Azure UI.
Login and general configuration
The very first step is to login to Azure and set the Microsoft Graph API scope:
az login --scope https://graph.microsoft.com/.default
The following providers need to be registered:
az provider register --namespace Microsoft.Compute
az provider register --namespace Microsoft.ContainerRegistry
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.DBforPostgreSQL
az provider register --namespace Microsoft.Storage
Self-signed certificate
Generate a self-signed certificate for the application:
This command uses the keystore filename (application.keystore) and passwords (password) used by default in WildFly's configuration (standalone.xml file). Using a stronger password is recommended.
az ad user show --id 'testuser1@contoso.onmicrosoft.com' --query id -o tsv
Groups
Create two groups orbeon-user and orbeon-admin:
az ad group create --display-name 'orbeon-user' --mail-nickname 'orbeon-user'
az ad group create --display-name 'orbeon-admin' --mail-nickname 'orbeon-admin'
Add users to groups:
az ad group member add --group 'orbeon-user' --member-id "$user1_id"
az ad group member add --group 'orbeon-user' --member-id "$user2_id"
az ad group member add --group 'orbeon-admin' --member-id "$user2_id"
Application
Create an application called Orbeon Forms. This is needed to expose our users and groups to Orbeon Forms via the OIDC protocol.
az ad app create --display-name 'Orbeon Forms' --sign-in-audience 'AzureADMyOrg'
Retrieve the application ID:
ENTRA_ID_APP_ID=$(az ad app list --query "[?displayName=='Orbeon Forms'].appId" -o tsv)
You can also retrieve it from the output of the previous command.
Add an API identifier URI to the application:
az ad app update --id "$ENTRA_ID_APP_ID" --identifier-uris "api://$ENTRA_ID_APP_ID"
Retrieve the application object ID:
ENTRA_ID_APP_OBJECT_ID=$(az ad app list --query "[?displayName=='Orbeon Forms'].id" -o tsv)
Note that this is not the same as the application ID (id vs appId).
To have access to the groups/roles in both the OIDC ID and access tokens, we need to add a scope to the application. If we don't do this, WildFly's OIDC implementation will be unable to retrieve the groups/roles.
Add a scope called groups.access to the application:
az rest \
--method PATCH \
--uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" \
--headers 'Content-Type=application/json' \
--body "$(jq -n \
--arg scope_id "$(uuidgen)" \
'{
api: {
oauth2PermissionScopes: [{
adminConsentDescription: "Allow the application to access groups on behalf of the signed-in user.",
adminConsentDisplayName: "Access groups",
id: $scope_id,
isEnabled: true,
type: "User",
userConsentDescription: "Allow the application to access groups on your behalf.",
userConsentDisplayName: "Access groups",
value: "groups.access"
}]
}
}')"
The scope ID is generated using uuidgen. Note that the jq command above is used to inject the scope ID into the JSON body. This can be done manually or in other ways as well.
For an existing scope, you can retrieve the scope ID from its name using the following command:
ENTRA_ID_SCOPE_ID=$(az ad app show \
--id "$ENTRA_ID_APP_ID" \
--query "api.oauth2PermissionScopes[?value=='groups.access'].id" \
-o tsv)
Pre-authorize the application, so that the users won't have to explicitly consent to the permissions:
az ad app show --id "$ENTRA_ID_APP_ID" | \
jq --arg app_id "$ENTRA_ID_APP_ID" \
--arg scope_id "$ENTRA_ID_SCOPE_ID" \
'.api.preAuthorizedApplications = [{
"appId": $app_id,
"delegatedPermissionIds": [$scope_id]
}]' | \
az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" --body @-
Add a client secret:
ENTRA_ID_CREDENTIAL_SECRET=$(az ad app credential reset \
--id "$ENTRA_ID_APP_ID" \
--display-name 'Orbeon Forms Credential' \
--years 2 | jq -r '.password')
Beware: the command above will update/overwrite any existing secret with the same name.
Only security group membership claims will be included as group IDs:
az ad app show --id "$ENTRA_ID_APP_ID" | \
jq '.groupMembershipClaims = "SecurityGroup"' | \
az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" --body @-
Add optional OIDC claims (groups included as roles, email):
Note that we have updated the application manifest multiple times, using the az rest --method PATCH command, but this was mainly done for demonstration purposes. In a real-world scenario, you would likely update the application manifest only once, with all the changes.
Grant Microsoft Graph permissions:
# Azure API constants
API_MICROSOFT_GRAPH='00000003-0000-0000-c000-000000000000'
API_PERMISSION_OPENID='37f7f235-527c-4136-accd-4a02d197296e'
API_PERMISSION_EMAIL='64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'
az ad app permission add \
--id "$ENTRA_ID_APP_ID" \
--api "$API_MICROSOFT_GRAPH" \
--api-permissions "$API_PERMISSION_OPENID=Scope"
az ad app permission add \
--id "$ENTRA_ID_APP_ID" \
--api "$API_MICROSOFT_GRAPH" \
--api-permissions "$API_PERMISSION_EMAIL=Scope"
Grant admin consent for permissions above:
az ad app permission admin-consent --id "$ENTRA_ID_APP_ID"
Configuration files
Get the tenant ID:
ENTRA_ID_TENANT_ID=$(az account show --query tenantId -o tsv)
The above configuration will return users as IDs. If you want to return users as emails, you can use email instead of oid as the value for principal-attribute.
In OIDC, we will refer to the groups by their IDs (not their display names):
ENTRA_ID_USER_GROUP_ID=$(az ad group show --group 'orbeon-user' --query id -o tsv)
ENTRA_ID_ADMIN_GROUP_ID=$(az ad group show --group 'orbeon-admin' --query id -o tsv)
Alternatively, you can call az postgres flexible-server create without --public-access None. This will automatically create the firewall rule above.
Create the orbeon database:
az postgres flexible-server db create \
--database-name `orbeon` \
--server-name "$DATABASE_SERVER" \
--resource-group 'orbeon-forms-resource-group';
Make the database administrator password available to the psql command:
export PGPASSWORD="$DATABASE_ADMIN_PASSWORD"
Create the orbeon database user:
psql \
--host "$DATABASE_SERVER.postgres.database.azure.com" \
--username "$DATABASE_ADMIN_USERNAME" \
--dbname `orbeon` \
--command "CREATE USER \"orbeon@$DATABASE_SERVER\" WITH PASSWORD '$password';"
Note that Azure PostgreSQL users need to follow the format username@servername.
Grant privileges to the orbeon database user:
psql \
--host "$DATABASE_SERVER.postgres.database.azure.com" \
--username "$DATABASE_ADMIN_USERNAME" \
--dbname `orbeon` \
--command "GRANT ALL PRIVILEGES ON DATABASE orbeon TO \"orbeon@$DATABASE_SERVER\";" \
--command "GRANT ALL PRIVILEGES ON SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
--command "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
--command "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
--command "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"orbeon@$DATABASE_SERVER\";" \
--command "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \"orbeon@$DATABASE_SERVER\";";
Note that the standalone.xml file is mounted in the container as docker-entrypoint-wildfly.d/standalone.xml. This is because WildFly needs to move/rename that file, so it needs to be copied to instead of mounted directly in the /opt/jboss/wildfly/standalone/configuration configuration directory. See this issue on GitHub for more information. The WildFly version of Orbeon Forms will copy any standalone.xml found in docker-entrypoint-wildfly.d to the WildFly configuration directory.
Import the deployment configuration:
kubectl apply -f orbeon-forms-deployment.yaml
You can display information about you Kubernetes contexts, cluster, pods, nodes, and service with the following commands:
# Contexts information
kubectl config get-contexts
# Cluster information
kubectl cluster-info
# Pods information
kubectl describe pods
# Nodes information
kubectl get nodes -o wide
# Service information
kubectl get service 'orbeon-forms-service'
Retrieve the cluster's external/public IP:
K8S_EXTERNAL_IP=$(kubectl get service 'orbeon-forms-service' --output jsonpath='{.status.loadBalancer.ingress[0].ip}')
Orbeon Forms will be available from the following URL:
K8S_APP_URL="https://$K8S_EXTERNAL_IP/orbeon"
Update the Entra ID redirect URIs with the actual Orbeon Forms URL:
az ad app update --id "$ENTRA_ID_APP_ID" --web-redirect-uris "$K8S_APP_URL/*"
Retrieve the Kubernetes pod name, which can be used to retrieve the logs (see below):
K8S_POD=$(kubectl get pod -o name | head -1)
Alternatively, you can use Azure Container Instances (ACI) for a simpler deployment, but with limited support for file mounts, port mappings, etc.
Private network
The last step is to configure a private network to allow the Kubernetes cluster to access the PostgreSQL database server.
Create a private DNS zone:
az network private-dns zone create \
--resource-group 'orbeon-forms-resource-group' \
--name 'private.postgres.database.azure.com'
The network resource group we will use below is the Kubernetes node resource group. It follows the following format: