Server-Submit (Draft Promotion via Service Principal)
Overview
The Server-Submit feature allows a trusted server-side process (authenticated via an Azure AD Service Principal JWT) to promote draft PF Form records into the target system, on behalf of the original end user. This enables automated workflows such as:
- Batch/offline approvals from external systems.
- Integration triggers by the backend system (D365, SalesForce, etc).
- Post-processing pipelines that need to submit drafts after checks in an external system.
Architecture
External Process
│
│ POST /dem/demdata/serversubmit/{customerid}/{environmentid}
│ Authorization: Bearer <SP JWT>
│ Body: { demdataId | formId+referenceNumber | rootRecordPrimaryKey }
▼
Server-Submit Azure Function
│
├── 1. Validate SP JWT (appid, signature, issuer, audience, lifetime)
│
├── 2. Authorize caller: appid == environment-level configured allowed app ID
│
├── 3. Resolve DEMData draft record (one of 3 lookup modes)
│
├── 4. Validate:
│ • Form allows server-side submission
│ • Skeleton record exists in the target system
│ • Draft not already promoted (duplicate protection)
│
├── 5. Execute promotion into the target system
│
└── 6. Write audit record (Azure Table)
Security Model
| Layer | Mechanism | Purpose |
|---|---|---|
| Function Host Key | Function-level access key | Basic Azure Functions access control |
| SP JWT | Bearer token in Authorization header |
Authenticates the calling service |
| OIDC Validation | Signature, issuer, audience, lifetime | Prevents token forgery/replay |
| Caller Authorization | appid claim compared against environment-level configured allowed app ID |
Restricts to approved service principals |
| Form-Level Gate | Form configuration flag | Form designer must opt in |
| Draft-Level Guard | Draft status field | Prevents double-promotion |
| Skeleton Requirement | Primary target record ID must exist | Ensures draft was initially submitted from client |
Service Principal Setup
Purpose
The Service Principal (Azure AD application) provides a non-interactive identity for server-to-server authentication. Unlike end-user JWTs (which require user interaction via browser login and consent), the SP JWT is obtained via the OAuth 2.0 client credentials flow — the external process authenticates using its own credentials (client secret or certificate) without any user present. This is the foundation of the trusted-server model: the SP's identity is known, registered, and auditable.
Creating the Service Principal
-
Register an Azure AD application
-
Navigate to Azure Active Directory → App registrations → New registration.
- Set a name (e.g.
dem-server-submit-prod). - Choose "Accounts in this organizational directory only" (single-tenant) unless cross-tenant access is required.
- No redirect URI is needed (this is a daemon/service app, not interactive).
-
Note the Application (client) ID — this is the value you configure in the environment settings for the allowed server-submit caller.
-
Configure credentials
Choose one of:
-
Client secret (simpler, but less secure for production):
- Go to Certificates & secrets → Client secrets → New client secret.
- Set expiry (recommend 6–12 months with rotation reminders).
- Copy the secret value immediately (it won't be shown again).
-
Certificate (recommended for production):
- Go to Certificates & secrets → Certificates → Upload certificate.
- Use a trusted certificate (self-signed is acceptable for non-production).
- The external process must have access to the private key and reference the certificate thumbprint.
-
Grant API permissions (default directory access)
The SP JWT is validated by the DEM Function itself (not Azure AD-gated), so the SP technically does not require delegated API permissions. However, for the client_credentials flow to work, the SP must be granted at least the default directory scope of the target audience:
- Go to API permissions → Add a permission → APIs my organization uses.
- Search for the DEM Function app registration (or use "Azure Active Directory Graph" → "default").
- Click Grant admin consent for the tenant.
Important: If the audience (
audclaim) in the SP JWT must match one of the Function host URLs, ensure your token request uses the correctaudienceparameter. The audience is resolved from theServerSubmitAuthAudienceenvironment setting, falling back to{scheme}://{host}at runtime.
The validator supports semi-colon separated audiences (aud1;aud2;aud3). When multiple audiences are provided, the token is accepted if it matches any one of them. This is useful when the same environment must accept tokens minted for different host names (e.g., during migration or multi-region deployments).
Similarly, the optional ServerSubmitAuthIssuer environment setting accepts a semi-colon separated list of trusted issuers, providing flexibility for multi-tenant or cross-tenant scenarios.
Obtaining a Token (External Process)
The calling system requests a JWT using the client credentials OAuth 2.0 flow:
POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id={sp-client-id}
&client_secret={sp-client-secret}
&scope={audience}/.default
&grant_type=client_credentials
Where:
| Parameter | Value |
|---|---|
{tenant-id} |
Your Azure AD tenant ID (or common for multi-tenant) |
{sp-client-id} |
The Application (client) ID from Step 1 |
{sp-client-secret} |
The secret value from Step 2 |
{audience} |
The Function host URL, e.g. https://my-func-app.azurewebsites.net |
grant_type |
Must be client_credentials |
The response includes an access_token that is passed as the Authorization: Bearer {token} header to the server-submit endpoint.
Token Contents (Decoded Example)
{
"aud": "https://my-func-app.azurewebsites.net",
"iss": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"iat": 1680000000,
"nbf": 1680000000,
"exp": 1680086400,
"appid": "11111111-2222-3333-4444-555555555555",
"appidacr": "1",
"tid": "{tenant-id}",
"oid": "..."
}
The critical claim is appid — this is validated against the environment-level configured allowed app ID.
Rotation and Security Practices
- Rotate secrets every 6 months or sooner (Azure AD supports overlapping secrets for zero-downtime rotation).
- Use certificates in production — they are more secure and have longer validity periods.
- Monitor audit logs: Check the
ServerSubmitAuditAzure Table for unexpectedRejectedorFailedoutcomes. - Restrict per environment: Use a separate SP per non-production vs. production environment, each with its own allowed app ID configured in the environment settings.
Lookup Strategies
Exactly one lookup strategy must be provided in the request body:
1. By DEMData ID (most efficient)
2. By Form ID + Reference Number
{
"formId": "npo_myform",
"referenceNumber": "REF-2026-00042",
"submittedByUserId": "user@contoso.com"
}
3. By Root Record Primary Key
{
"rootRecordPrimaryKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"submittedByUserId": "user@contoso.com"
}
Setup Checklist
-
Configure the allowed Service Principal app ID in the environment settings for the target environment. Set this to the Application ID (client ID) of the Azure AD Service Principal that will call the endpoint.
-
Configure the environment settings for the target environment (via the settings portal):
| Setting | Description | Example |
|---|---|---|
ServerSubmitAllowedAppId |
The Application (client) ID of the Azure AD Service Principal authorized to call the server-submit endpoint | 11111111-2222-3333-4444-555555555555 |
ServerSubmitAuthAuthority |
(Optional) Azure AD authority URL for OIDC metadata discovery. Supports national clouds. Falls back to https://login.microsoftonline.com/common/v2.0. |
https://login.microsoftonline.com/{tenant-id}/v2.0 |
ServerSubmitAuthAudience |
(Optional) Expected audience (aud claim) — supports semi-colon separated list. Falls back to the Function App's host URL. |
api://func-app.azurewebsites.net or aud1;aud2 |
ServerSubmitAuthIssuer |
(Optional) Expected issuer (iss claim) — supports semi-colon separated list. Falls back to the issuer from OIDC metadata. |
https://login.microsoftonline.com/{tenant-id}/v2.0;https://login.microsoftonline.com/{other-tenant}/v2.0 |
-
Enable server-side submission on each DEM Form that should accept server-side promotion.
-
Grant the Service Principal function-level access key permission to the server-submit endpoint.
-
Ensure the draft has a skeleton record — the draft must be submitted from the client at least once (even if partially) so that the primary target record ID is populated in target DB.
Request Example
POST https://{function-app}.azurewebsites.net/api/dem/demdata/serversubmit/contoso/env-prod
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
x-master-correlation-id: my-journey-001
Content-Type: application/json
{
"demdataId": "64a1b2c3d4e5f67890123456",
"submittedByUserId": "user@contoso.com"
}
Response Examples
Success (200 OK)
{
"content": {
"outcome": "Promoted",
"demDataId": "64a1b2c3d4e5f67890123456",
"referenceNumber": "REF-2026-00042",
"targetRecordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
Rejected (401 Unauthorized)
Possible rejection reasons:
- "This record has already been promoted via server-submit."
- "This draft does not have a skeleton record in the target system. Submit the draft from the client first."
- "This form does not allow server-side submission."
- "Caller appid '{...}' is not authorized for server-submit in this environment."
- "Server-submit is not enabled for this environment."
Validation Error (400 Bad Request)
{
"error": "No lookup strategy provided. Provide demdataId, formId+referenceNumber, or rootRecordPrimaryKey."
}
Audit Trail
Every server-submit attempt writes a record to the ServerSubmitAudit Azure Table with:
| Field | Description |
|---|---|
EnvironmentId |
The environment where the promotion was attempted |
DEMDataId |
The draft record ID |
CallerAppId |
The Service Principal appid that made the request |
SubmittedByUserId |
The user identifier supplied by the calling service (e.g. CRM user email or systemuserid) |
RequestTimestampUtc |
UTC timestamp of the attempt |
Outcome |
Success, Rejected, or Failed |
TargetRecordReference |
Primary Key of the record in the target record store |
FailureReason |
Error or rejection detail |
MasterCorrelationId |
Cross-system tracing ID |