Skip to content

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

  1. Register an Azure AD application

  2. Navigate to Azure Active DirectoryApp registrationsNew registration.

  3. Set a name (e.g. dem-server-submit-prod).
  4. Choose "Accounts in this organizational directory only" (single-tenant) unless cross-tenant access is required.
  5. No redirect URI is needed (this is a daemon/service app, not interactive).
  6. Note the Application (client) ID — this is the value you configure in the environment settings for the allowed server-submit caller.

  7. Configure credentials

Choose one of:

  • Client secret (simpler, but less secure for production):

    • Go to Certificates & secretsClient secretsNew 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 & secretsCertificatesUpload 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 permissionsAdd a permissionAPIs 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 (aud claim) in the SP JWT must match one of the Function host URLs, ensure your token request uses the correct audience parameter. The audience is resolved from the ServerSubmitAuthAudience environment 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 ServerSubmitAudit Azure Table for unexpected Rejected or Failed outcomes.
  • 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)

{
  "demdataId": "abc123-def456",
  "submittedByUserId": "user@contoso.com"
}

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

  1. 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.

  2. 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
  1. Enable server-side submission on each DEM Form that should accept server-side promotion.

  2. Grant the Service Principal function-level access key permission to the server-submit endpoint.

  3. 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)

{
  "error": "This record has already been promoted via server-submit."
}

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