πŸ“œ Citadel Access Contracts - Use-case onboarding

Overview

Automate the onboarding of AI use cases to your APIM-based AI Gateway with a streamlined, infrastructure-as-code approach using Bicep parameter files (.bicepparam).

This package eliminates manual APIM configuration by providing:

  • πŸ“¦ Automated Product Creation: Per-service APIM products with naming <serviceCode>-<BU>-<UseCase>-<ENV>
  • πŸ”Œ API Integration: Automatic API attachment to product with custom or default policies
  • πŸ”‘ Subscription Management: Auto-generated subscription with secure API keys
  • πŸ” Flexible Secret Storage: Optional Azure Key Vault integration or direct credential output
  • πŸ€– Azure Microsoft Foundry Integration: Optional APIM connection creation for Foundry agents
  • πŸ“ Declarative Configuration: Simple .bicepparam & .xml files for version control per use case
  • πŸ” JWT Authentication: Optional layered API Key + JWT Bearer token authentication per product

What Gets Created

Resource Naming Pattern Description
APIM Product {code}-{BU}-{UseCase}-{ENV} Product per service (e.g., LLM-Healthcare-PatientAssistant-DEV) with attached APIs and policies
APIM Subscription {product}-SUB-01 Subscription with API key
Key Vault Secrets {secretName} Endpoint URL and API key (optional)
Foundry Connection {prefix}-{code} APIM connection for Microsoft Foundry agents (optional)

Key Features

✨ Simplified Parameters: No need for full resource IDs - just API names
πŸ”„ Optional Key Vault: Choose between Key Vault storage or direct output
πŸ€– Optional Foundry Integration: Create APIM connections for AI agents
πŸ“‹ Policy Templates: Pre-built policies for common use cases
🎯 Multi-Service Support: Onboard multiple AI services in one deployment
πŸ”’ Secure by Default: Credentials stored in Key Vault or marked as secrets
πŸ“Š Production Ready: Designed for scale and aligned with DevOps practices β€”

Deployment quick reference

below are the high-level steps to deploy the use case onboarding Bicep package before diving into the detailed documentation:

  1. Create a folder dedicated for use-cases contracts: a folder like contracts in the citadel-access-contracts module to hold your use case specific files.
  2. Create a use-case contract folder: under the contracts folder, create a new folder for your use case following the pattern <businessunit>-<usecasename> (e.g. sales-assistant, hr-chatagent).
  3. Create an environment subfolder: under the use case folder, create a subfolder for each environment (e.g. dev, test, prod).
  4. Prepare Parameter File: Create new use case .bicepparam file (you can use main.bicepparam as a base) under the environment folder.
  5. Create/Customize APIM Policy: Use default or create a custom XML policy. For simplicity, policy file can be named ai-product-policy.xml and placed in the same environment folder.
  6. Deploy template with the prepared parameter file:
    # This can be executed in CLI or through a DevOps pipeline
    az deployment sub create --name <use-case-contract-name> --location <location> --template-file main.bicep --parameters contracts/<businessunit-usecasename>/<environment>/main.bicepparam
    

NOTE: Ensure that you are updating values according to your environment and folder structure.

πŸ—ΊοΈ Architecture Overview

Deployment Flow

flowchart TB
    subgraph Input["πŸ“₯ Inputs"]
        P1[bicepparam file]
        P5[Policy XML files]
    end

    subgraph Deploy["πŸš€ Deployment"]
        D1[main.bicep]
        D2[Create Products]
        D3[Attach APIs to Products]
        D4[Apply Policies]
        D5[Create Subscriptions]
        D6{Use Key Vault?}
        D7[Store Secrets in KV]
        D8[Output Credentials]
        D9{Use Foundry?}
        D10[Create Foundry Connections]
    end

    subgraph Output["πŸ“€ Outputs"]
        O1[Products Created]
        O2[Subscription Keys]
        O3[KV Secret Names]
        O4[Direct Credentials]
        O5[Foundry Connections]
    end

    Input --> Deploy
    D1 --> D2 --> D3 --> D4 --> D5 --> D6
    D6 -->|Yes| D7 --> O3
    D6 -->|No| D8 --> O4
    D5 --> D9
    D9 -->|Yes| D10 --> O5
    D9 -->|No| O2
    D2 --> O1
    D5 --> O2

Runtime Request Flow

Below is a suggested flow for client applications (i.e. agents) interacting with the onboarded services via the Citadel Access Contracts:

sequenceDiagram
    participant App as AI Agent/App
    participant Entra as Microsoft Entra ID
    participant KV as Azure Key Vault
    participant Foundry as Microsoft Foundry
    participant APIM as AI Gateway
    participant AI as AI Services

    alt Using Key Vault
        App->>KV: Get endpoint + API key
        KV-->>App: Return secrets
    else Using Foundry Connection
        App->>Foundry: Use APIM connection
        Foundry->>Foundry: Get stored credentials
    else Direct Credentials
        Note over App: Use credentials from deployment output
    end

    opt JWT Authentication Enabled
        App->>Entra: Request JWT token (client credentials)
        Entra-->>App: Return Bearer token
    end
    
    App->>APIM: HTTPS request with api-key + optional Bearer token
    APIM->>APIM: Validate subscription key (always required)
    APIM->>APIM: Apply product policy (check jwtRequired)
    opt JWT Required by Product
        APIM->>APIM: Validate JWT via security-handler fragment
    end
    APIM->>AI: Forward to backend service
    AI-->>APIM: Response
    APIM->>APIM: Logs & metrics
    APIM-->>App: Response with usage headers

πŸ“ Repository Structure

This is a submodule focused on Citadel Access Contracts. The folder structure is as follows:

citadel-access-contracts/
β”œβ”€β”€ main.bicep                          # Main orchestration template
β”œβ”€β”€ main.bicepparam                     # Base parameter file
β”œβ”€β”€ modules/
β”‚   β”œβ”€β”€ apimOnboardService.bicep        # Product + subscription creation
β”‚   β”œβ”€β”€ apimProduct.bicep               # APIM product module
β”‚   β”œβ”€β”€ apimSubscription.bicep          # Subscription module
β”‚   β”œβ”€β”€ kvSecrets.bicep                 # Key Vault secret storage
β”‚   └── foundryConnection.bicep         # Azure Microsoft Foundry connection module
β”œβ”€β”€ policies/
β”‚   └── default-ai-product-policy.xml   # Default product policy
β”œβ”€β”€ contracts/                          # Use-case contracts folder for source control 
β”‚   └── <businessunit-usecasename>/     # Use case folder (e.g., sales-assistant)
β”‚       β”œβ”€β”€ dev/                        # Environment subfolder
β”‚       β”‚   β”œβ”€β”€ main.bicepparam         # Use case specific parameters
β”‚       β”‚   └── ai-product-policy.xml   # Custom policy file
β”‚       β”œβ”€β”€ test/                       # Test environment
β”‚       β”‚   β”œβ”€β”€ main.bicepparam
β”‚       β”‚   └── ai-product-policy.xml
β”‚       └── prod/                       # Production environment
β”‚           β”œβ”€β”€ main.bicepparam
β”‚           └── ai-product-policy.xml

πŸ”§ Parameter File Reference

Main Parameters (main.bicepparam)

Parameter Type Required Description Example
apim object βœ… APIM instance coordinates { subscriptionId, resourceGroupName, name }
keyVault object βœ…* Key Vault for secrets (*required even if not used) { subscriptionId, resourceGroupName, name }
useTargetAzureKeyVault bool ❌ Store secrets in Key Vault (default: false) true or false
useCase object βœ… Use case naming context { businessUnit, useCaseName, environment }
apiNameMapping object βœ… Map service codes to API names { OAI: ["azure-openai-service-api"], ... }
services array βœ… Services to onboard See Services Schema below
productTerms string ❌ Product terms of service β€œBy using this product…”

JWT Authentication: JWT validation is configured per access contract via the policies.jwtAuth.enabled field in the Agent Access Contract Request JSON (see Base Access Contract Request). When enabled, the product policy sets jwtRequired=true, and the security-handler fragment enforces JWT Bearer token validation in addition to the API key.

useTargetFoundry bool ❌ Create Foundry connections (default: false) true or false
foundry object ❌* Microsoft Foundry coordinates (*required if useTargetFoundry=true) { subscriptionId, resourceGroupName, accountName, projectName }
foundryConfig object ❌ Foundry connection configuration See Foundry Config below

Service Code mapping

Map service codes (which is a short acronym that represents the category of services fall under) to their API-id in APIM:

{
  LLM: ["azure-openai-api", "universal-llm-api"]
  OAIRT: ["openai-realtime-ws-api"]
  DOC: ["document-intelligence-api", "document-intelligence-api-legacy"]
  SRCH: ["azure-ai-search-index-api"]
  // ... add more services
}

Note: API-id must already exist in your APIM instance. The deployment will fail if an API name is not found.

Above is the list of supported APIs that are provisioned by default by the accelerator.

Adding custom APIs

You can onboard any number of other APIs as well to support your custom services. Add the newly added API-ids to the mapping above accordingly.

Multi-service Bundles

Mapping currently is suggested to focus on a specific category of services (e.g., LLM, Document Intelligence, etc.)

You can create unique mappings that mix different service types under one bundle if needed (this will require the product policy to be aware of that mix to apply the correct policies based on the service type like using tokens-per-mint limits for LLM and request-per-min limits for Document Intelligence).

Use-case service assignment schema

Each service in the services array:

{
  code: string              // Service code (e.g., "LLM", "DOC", "SRCH")
  endpointSecretName: string // Name for endpoint secret in Key Vault
  apiKeySecretName: string   // Name for API key secret in Key Vault
  policyXml: string          // Optional: Custom policy XML (empty = use default)
}

This will create one APIM product + subscription + Key Vault secrets per service code and leverage the referenced policy XML for that product.

This is an array to allow multiple services to be assigned to specific use case (like granting both LLM and document intelligence access to the same application).

But each service will have its own product + subscription + secrets (i.e llm will have a different key from document intelligence).


🧱 What gets created

Component Scope Naming Notes
APIM Product APIM <serviceCode>-<BU>-<UseCase>-<ENV> One per service code you include
APIM Subscription APIM <product>-SUB-01 Primary key is captured into Key Vault
Key Vault Secrets KV endpointSecretName, apiKeySecretName One endpoint + one key per service
Foundry Connection Microsoft Foundry <prefix>-<serviceCode> One connection per service (if enabled)

Naming examples

  • Product: LLM-Retail-FinancialAssistant-DEV
  • Subscription: LLM-Retail-FinancialAssistant-DEV-SUB-01
  • Foundry Connection: Retail-FinancialAssistant-DEV-LLM

βœ… Prerequisites

Azure Resources

Resource Requirement How to Verify
Citadel Compliant APIM Instance with published APIs matching your apiNameMapping az apim api list -g <rg> -n <apim-name>
Azure Key Vault Accessible with secret set permissions (if using KV) az keyvault show -n <kv-name>
Azure Microsoft Foundry Account and project must exist (if using Foundry) az cognitiveservices account show -n <account-name> -g <rg>

Permissions Required

The deployment identity needs:

Scope Role Purpose
APIM Resource Group API Management Service Contributor Create products and subscriptions
Target Key Vault (if used) Key Vault Secrets Officer Write secrets
Microsoft Foundry Resource Group (if used) Contributor Create connections
Subscription Reader Reference existing resources

⚑ Quick Start Guide

Step 1: Create use case folder with environment subfolder

Create a folder structure following the pattern contracts/<businessunit-usecasename>/<environment>/ with copies of both the main.bicepparam and default policy as a base.

# Create the folder structure: contracts/healthcare-chatbot/dev/
mkdir -p bicep/infra/citadel-access-contracts/contracts/healthcare-chatbot/dev
cd bicep/infra/citadel-access-contracts/contracts/healthcare-chatbot/dev

# Copy template files (note: path goes up 3 levels due to environment subfolder)
cp ../../../main.bicepparam main.bicepparam
cp ../../../policies/default-ai-product-policy.xml ai-product-policy.xml

Step 2: Configure Your Parameters

# Edit main.bicepparam in the environment folder
code main.bicepparam

Update these values:

using '../../../main.bicep'

param apim = {
  subscriptionId: 'YOUR-SUBSCRIPTION-ID'        // ← Update
  resourceGroupName: 'YOUR-APIM-RESOURCE-GROUP'  // ← Update
  name: 'YOUR-APIM-NAME'                        // ← Update
}

param keyVault = {
  subscriptionId: 'YOUR-SUBSCRIPTION-ID'        // ← Update
  resourceGroupName: 'YOUR-KV-RESOURCE-GROUP'   // ← Update
  name: 'YOUR-KV-NAME'                          // ← Update
}

param useTargetAzureKeyVault = true  // false to output credentials directly

param useCase = {
  businessUnit: 'YourDepartment'     // ← Update
  useCaseName: 'YourUseCaseName'     // ← Update
  environment: 'DEV'                 // DEV, TEST, PROD
}

// Verify these API names exist in your APIM
param apiNameMapping = {
  LLM: ['azure-openai-api', 'universal-llm-api']
  DOC: ['document-intelligence-api', 'document-intelligence-api-legacy']
}

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'OPENAI-ENDPOINT'
    apiKeySecretName: 'OPENAI-API-KEY'
    policyXml: loadTextContent('ai-product-policy.xml')  // or '' for default
  }
  // Add more services as needed
]

Step 3: Validate Configuration

# Preview what will be created (run from the environment folder)
az deployment sub what-if `
  --location swedencentral `
  --template-file ../../../main.bicep `
  --parameters main.bicepparam

Step 4: Deploy

# Deploy at subscription scope (run from the environment folder)
az deployment sub create `
  --name healthcare-chatbot-dev-onboarding `
  --location swedencentral `
  --template-file ../../../main.bicep `
  --parameters main.bicepparam

# Or run from the citadel-access-contracts root folder:
az deployment sub create `
  --name healthcare-chatbot-dev-onboarding `
  --location swedencentral `
  --template-file main.bicep `
  --parameters contracts/healthcare-chatbot/dev/main.bicepparam

Step 5: Verify Deployment

# Check products created
az apim product list `
  --resource-group YOUR-APIM-RG `
  --service-name YOUR-APIM-NAME `
  --query "[?contains(name, 'REPLACE-PRODUCT-NAME')].{Name:name, State:state}"

# If using Key Vault, check secrets
az keyvault secret list `
  --vault-name YOUR-KV-NAME `
  --query "[?contains(name, 'openai')].name"

Step 6: Use the Service

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

# Get credentials from Key Vault
credential = DefaultAzureCredential()
kv_client = SecretClient(
    vault_url="https://YOUR-KV-NAME.vault.azure.net/",
    credential=credential
)

endpoint = kv_client.get_secret("openai-endpoint").value
api_key = kv_client.get_secret("openai-api-key").value

# Use with your application
print(f"Endpoint: {endpoint}")
# api_key is ready to use as api-key header

Also you can use the citadel-access-contracts-tests notebook to validate end-to-end connectivity of the newly created access contract.


πŸ”‘ Secret Management Options

Access contract generates sensitive keys for each service onboarded. You have two options to manage these secrets:

When to use: Production deployments, applications with managed identities

param useTargetAzureKeyVault = true

param keyVault = {
  subscriptionId: 'YOUR-SUB-ID'
  resourceGroupName: 'YOUR-KV-RG'
  name: 'YOUR-KV-NAME'
}

Benefits:

  • βœ… Centralized secret management
  • βœ… Automatic rotation support
  • βœ… Access auditing
  • βœ… Integration with managed identities

Retrieval:

from azure.keyvault.secrets import SecretClient
kv_client = SecretClient(vault_url="https://<kv-name>.vault.azure.net/", credential=credential)
endpoint = kv_client.get_secret("llm-endpoint").value

Option 2: Direct Output (CI/CD)

When to use: CI/CD pipelines, serverless functions, non-Azure environments where these values needs to be stored in some other secret store

param useTargetAzureKeyVault = false

// keyVault still required but can use placeholders as values are not considered.
param keyVault = {
  subscriptionId: '00000000-0000-0000-0000-000000000000'
  resourceGroupName: 'placeholder'
  name: 'placeholder'
}

Benefits:

  • βœ… No Key Vault dependency
  • βœ… Direct credential access
  • βœ… Works in any environment

Retrieval from Deployment Output:

$output = az deployment sub show `
  --name my-deployment `
  --query properties.outputs.endpoints.value -o json | ConvertFrom-Json

$oaiEndpoint = ($output | Where-Object { $_.code -eq 'LLM' }).endpoint
$oaiKey = ($output | Where-Object { $_.code -eq 'LLM' }).apiKey

# Store in CI/CD variables (NOT RECOMMENDED. Just for demonstration)
Write-Host "##vso[task.setvariable variable=LLM_ENDPOINT;issecret=true]$oaiEndpoint"
Write-Host "##vso[task.setvariable variable=LLM_KEY;issecret=true]$oaiKey"

⚠️ Security Note: When using direct output, handle credentials as secrets in your CI/CD system.

When to use: Building AI agents in Microsoft Foundry that need APIM gateway access

param useTargetFoundry = true

param foundry = {
  subscriptionId: 'YOUR-FOUNDRY-SUB-ID'
  resourceGroupName: 'YOUR-FOUNDRY-RG'
  accountName: 'YOUR-FOUNDRY-ACCOUNT'
  projectName: 'YOUR-PROJECT-NAME'
}

param foundryConfig = {
  connectionNamePrefix: ''        // Empty = use useCase naming
  deploymentInPath: 'false'       // Model in request body
  isSharedToAll: false            // Share with project users
  inferenceAPIVersion: ''         // APIM defaults
  deploymentAPIVersion: ''        // APIM defaults
  staticModels: []                // Dynamic discovery
  listModelsEndpoint: ''          // APIM defaults
  getModelEndpoint: ''            // APIM defaults
  deploymentProvider: ''          // AzureOpenAI format
  customHeaders: {}               // No custom headers
  authConfig: {}                  // Default api-key header
}

Benefits:

  • βœ… Seamless integration with Microsoft Foundry agents
  • βœ… Credentials stored securely in Foundry connection
  • βœ… Supports the β€œBring Your Own AI Gateway” pattern
  • βœ… Automatic model discovery from APIM
  • βœ… No need to manage secrets separately

Usage in Foundry Agent:

import os
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential

# Connection name follows pattern: <prefix>-<serviceCode>
# Example: HR-ChatBot-DEV-LLM
connection_name = "HR-ChatBot-DEV-LLM"
model_deployment = f"{connection_name}/gpt-4o"

os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] = model_deployment

client = AIProjectClient(
    credential=DefaultAzureCredential(),
    endpoint="https://your-foundry.cognitiveservices.azure.com/"
)

# Create agent using the APIM connection
agent = client.agents.create_agent(
    model=model_deployment,
    name="my-hr-assistant",
    instructions="You are a helpful HR assistant."
)

Foundry Configuration Options

Option Type Default Description
connectionNamePrefix string '' Custom prefix for connection names. Empty uses <BU>-<UseCase>-<ENV>
deploymentInPath string 'false' 'true': model in URL, 'false': model in body
isSharedToAll bool false Share connection with all project users
inferenceAPIVersion string '' API version for chat/embeddings (empty = APIM defaults)
deploymentAPIVersion string '' API version for discovery (empty = APIM defaults)
staticModels array [] Fixed model list (skips discovery)
listModelsEndpoint string '' Custom list endpoint (empty = /deployments)
getModelEndpoint string '' Custom get endpoint (empty = /deployments/{id})
deploymentProvider string '' Discovery format (AzureOpenAI or OpenAI)
customHeaders object {} Additional headers for requests
authConfig object {} Custom auth header config

Combined Targets Example

You can use Key Vault AND Foundry together:

// Store secrets in Key Vault for traditional apps
param useTargetAzureKeyVault = true
param keyVault = {
  subscriptionId: 'YOUR-SUB-ID'
  resourceGroupName: 'YOUR-KV-RG'
  name: 'YOUR-KV-NAME'
}

// Also create Foundry connections for AI agents
param useTargetFoundry = true
param foundry = {
  subscriptionId: 'YOUR-FOUNDRY-SUB-ID'
  resourceGroupName: 'YOUR-FOUNDRY-RG'
  accountName: 'YOUR-FOUNDRY-ACCOUNT'
  projectName: 'YOUR-PROJECT-NAME'
}

This creates:

  • APIM products and subscriptions
  • Key Vault secrets for traditional applications
  • Foundry APIM connections for AI agents

All using the same subscription keys, ensuring consistent governance.


πŸ“ Creating Custom Policies

Using Default Policy

The simplest approach - omit policyXml or set it to empty string:

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'LLM-ENDPOINT'
    apiKeySecretName: 'LLM-KEY'
    policyXml: ''  // Uses policies/default-ai-product-policy.xml
  }
]

Default policy includes:

  • Model restrictions (GPT-4o, deepseek-r1)
  • Token limits (300 tokens/min + 10,000 tokens/month)
  • Content safety checks

Creating Custom Policy

Step 1: Create policy XML file in your use case folder

You can use the default-ai-product-policy.xml as a base policy for LLM and extend/modify it as needed.

Additional policy capabilities can be found in Citadel-Access-Contracts-Policy.md for more options and patterns that can be leveraged.

Note: APIM has power policy engine that can be used even beyond what is provided in Citadel Governance Hub. Leverage agentic development like GitHub Copilot to help you create custom policies based on your requirements.

Step 2: Reference policy in bicepparam

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'LLM-ENDPOINT'
    apiKeySecretName: 'LLM-KEY'
    policyXml: loadTextContent('my-custom-policy.xml')
  }
]

πŸ”„ Advanced Scenarios

Multiple Services in One Use Case

Onboard multiple AI services simultaneously:

param apiNameMapping = {
  LLM: ['azure-openai-api', 'universal-llm-api']
  DOC: ['document-intelligence-api', 'document-intelligence-api-legacy']
  SRCH: ['azure-ai-search-index-api']
  OAIRT: ['openai-realtime-ws-api']
}

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'MULTI-LLM-ENDPOINT'
    apiKeySecretName: 'MULTI-LLM-KEY'
    policyXml: loadTextContent('llm-policy.xml')
  }
  {
    code: 'DOC'
    endpointSecretName: 'MULTI-DOC-ENDPOINT'
    apiKeySecretName: 'MULTI-DOC-KEY'
    policyXml: loadTextContent('doc-policy.xml')
  }
  {
    code: 'SRCH'
    endpointSecretName: 'MULTI-SEARCH-ENDPOINT'
    apiKeySecretName: 'MULTI-SEARCH-KEY'
    policyXml: ''  // Use default
  }
]

Result: Creates 3 APIM products, 3 subscriptions, 6 Key Vault secrets (endpoint + key for each service).


πŸ“€ Deployment Outputs

After deployment, the following outputs are available:

When Using Key Vault (useTargetAzureKeyVault = true)

Output Type Description Example
apimGatewayUrl string APIM gateway base URL https://apim-gateway.azure-api.net
useKeyVault bool Always true true
products[] array Created products [{ productId: "OAI-Healthcare-...", displayName: "..." }]
subscriptions[] array KV secret names [{ name: "OAI-...-SUB-01", keyVaultApiKeySecretName: "openai-api-key", ... }]

Access secrets from Key Vault:

$secretNames = (az deployment sub show --name my-deployment --query properties.outputs.subscriptions.value -o json | ConvertFrom-Json)
$endpoint = az keyvault secret show --vault-name <kv-name> --name ($secretNames[0].keyVaultEndpointSecretName) --query value -o tsv

When NOT Using Key Vault (useTargetAzureKeyVault = false)

Output Type Description Contains Secrets
apimGatewayUrl string APIM gateway base URL No
useKeyVault bool Always false No
products[] array Created products No
endpoints[] array Direct credentials ⚠️ YES

Endpoints output structure:

[
  {
    "code": "OAI",
    "productId": "OAI-Healthcare-PatientAssistant-DEV",
    "subscriptionName": "OAI-Healthcare-PatientAssistant-DEV-SUB-01",
    "endpoint": "https://apim-gateway.azure-api.net/openai",
    "apiKey": "abc123...xyz"  // ⚠️ Sensitive
  }
]

Extract credentials:

$output = az deployment sub show --name my-deployment --query properties.outputs.endpoints.value -o json | ConvertFrom-Json
$oaiCreds = $output | Where-Object { $_.code -eq 'OAI' }

# Store securely in CI/CD
Write-Host "##vso[task.setvariable variable=OAI_ENDPOINT;issecret=true]$($oaiCreds.endpoint)"
Write-Host "##vso[task.setvariable variable=OAI_KEY;issecret=true]$($oaiCreds.apiKey)"

⚠️ Security Warning: The endpoints output contains sensitive API keys when not using Key Vault. Always:

  • Mark as secrets in CI/CD systems
  • Never log or display in plaintext
  • Store in secure secret management systems
  • Rotate keys regularly

οΏ½ JWT Authentication for Access Contracts

Access Contracts support layered authentication: API Key is always required, with optional JWT Bearer token validation per product. This is configured during contract creation, not at deployment time.

Prerequisites

  • The Citadel Governance Hub must be deployed with entraAuth=true so that APIM JWT named values (JWT-TenantId, JWT-AppRegistrationId, JWT-Issuer, JWT-OpenIdConfigUrl) and the security-handler policy fragment are provisioned
  • An Entra ID App Registration exists (auto-provisioned by the entra-id Bicep module or provided manually)
  • Client secret is stored in Key Vault as ENTRA-APP-CLIENT-SECRET

How It Works

  1. The product policy snippet jwt-auth.xml sets jwtRequired=true in the APIM context
  2. The security-handler fragment detects the auth method (api-key, jwt, api-key-jwt)
  3. When jwtRequired=true, the fragment validates the JWT Bearer token against the Entra ID OpenID configuration
  4. Requests without a valid JWT are rejected with 401 Unauthorized

Authentication Flow

Scenario Headers Required Result
API Key only (JWT not enabled) api-key: {key} βœ… Allowed
API Key + JWT (JWT enabled) api-key: {key} + Authorization: Bearer {token} βœ… Allowed
API Key only (JWT enabled) api-key: {key} ❌ 401 - JWT required
JWT only (no API Key) Authorization: Bearer {token} ❌ 401 - API key required
Invalid JWT (JWT enabled) api-key: {key} + Authorization: Bearer {invalid} ❌ 401 - Invalid token

Enabling JWT via Agent Access Contract Request

When using the base-access-contract-request module, enable JWT in the contract JSON:

{
  "contractInfo": {
    "businessUnit": "Security",
    "useCaseName": "SecureAgent",
    "environment": "DEV"
  },
  "policies": {
    "jwtAuth": { "enabled": true },
    "modelAccess": {
      "enabled": true,
      "allowedModels": ["gpt-4o", "gpt-4o-mini"]
    }
  },
  "services": [
    {
      "code": "LLM",
      "endpointSecretName": "SECURE-AGENT-ENDPOINT",
      "apiKeySecretName": "SECURE-AGENT-KEY"
    }
  ]
}

Enabling JWT via Custom Product Policy

For manual access contracts (not using the agent request module), add the JWT snippet to your product policy XML:

<policies>
    <inbound>
        <base />
        <!-- Enable JWT requirement for this product -->
        <set-variable name="jwtRequired" value="true" />
        
        <!-- Other policy snippets (model access, capacity, etc.) -->
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Acquiring a JWT Token

Use the client credentials flow to obtain a JWT token from Microsoft Entra ID:

POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={entra-app-client-id}
&client_secret={entra-app-client-secret}
&scope={audience}/.default

Then include the token in API requests:

POST https://{apim-gateway}/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-15-preview
api-key: {subscription-key}
Authorization: Bearer {jwt-token}
Content-Type: application/json

Validation

Use the JWT Access Contract Validation Notebook to test end-to-end JWT authentication with access contracts.


οΏ½πŸ“ž Support

For issues or questions: