π 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&.xmlfiles 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:
- Create a folder dedicated for use-cases contracts: a folder like
contractsin the citadel-access-contracts module to hold your use case specific files. - 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). - Create an environment subfolder: under the use case folder, create a subfolder for each environment (e.g.
dev,test,prod). - Prepare Parameter File: Create new use case
.bicepparamfile (you can usemain.bicepparamas a base) under the environment folder. - Create/Customize APIM Policy: Use default or create a custom XML policy. For simplicity, policy file can be named
ai-product-policy.xmland placed in the same environment folder. - 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.enabledfield in the Agent Access Contract Request JSON (see Base Access Contract Request). When enabled, the product policy setsjwtRequired=true, and thesecurity-handlerfragment 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:
Option 1: Use Key Vault (Recommended)
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.
Option 3: Use Microsoft Foundry Connections (Recommended for Agents)
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=trueso that APIM JWT named values (JWT-TenantId,JWT-AppRegistrationId,JWT-Issuer,JWT-OpenIdConfigUrl) and thesecurity-handlerpolicy fragment are provisioned - An Entra ID App Registration exists (auto-provisioned by the
entra-idBicep module or provided manually) - Client secret is stored in Key Vault as
ENTRA-APP-CLIENT-SECRET
How It Works
- The product policy snippet
jwt-auth.xmlsetsjwtRequired=truein the APIM context - The
security-handlerfragment detects the auth method (api-key,jwt,api-key-jwt) - When
jwtRequired=true, the fragment validates the JWT Bearer token against the Entra ID OpenID configuration - 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:
- GitHub Issues: Report bugs or request features