Skip to content

Commit cbb0d46

Browse files
authored
Merge pull request #57 from Azure-Samples/feat/keyless-auth-local-dev
feat: implement keyless authentication for local development
2 parents 51f1306 + b405912 commit cbb0d46

File tree

8 files changed

+226
-27
lines changed

8 files changed

+226
-27
lines changed

LOCAL_DEVELOPMENT.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Local Development Setup
2+
3+
This document explains how local development authentication works for this Azure Functions project.
4+
5+
## Authentication Approach
6+
7+
This project uses **Azure Managed Identity** for authentication - both in production AND for local development. **No API keys are required!**
8+
9+
### How It Works
10+
11+
1. **In Azure (Production)**: The Function App uses its User-Assigned Managed Identity with the `Cognitive Services OpenAI User` role
12+
2. **Locally**: Developers use their own Azure credentials via `az login` with the same role
13+
14+
## Setup Steps
15+
16+
### 1. Run `azd up` or `azd provision`
17+
18+
When you provision the infrastructure, the Bicep deployment automatically:
19+
- Creates the Azure OpenAI resource
20+
- Assigns the `Cognitive Services OpenAI User` role to the Function App's managed identity
21+
- **Assigns the same role to YOU** (the person running `azd provision`)
22+
23+
This is done via the `principalId` parameter in `main.bicep`:
24+
25+
```bicep
26+
// This assigns the role to the person running azd provision
27+
module openaiRoleAssignmentDeveloper 'app/rbac/openai-access.bicep' = if (!empty(principalId)) {
28+
name: 'openaiRoleAssignmentDeveloper'
29+
scope: rg
30+
params: {
31+
openAIAccountName: openai.outputs.aiServicesName
32+
roleDefinitionId: CognitiveServicesOpenAIUser
33+
principalId: principalId // Your user ID
34+
}
35+
}
36+
```
37+
38+
### 2. Make Sure You're Logged In
39+
40+
```bash
41+
# Login to Azure
42+
az login
43+
44+
# Verify you're using the correct subscription
45+
az account show
46+
```
47+
48+
### 3. Generate local.settings.json
49+
50+
```bash
51+
# Run the postprovision script
52+
./scripts/generate-settings.sh
53+
54+
# Verify it was created (notice: no API key!)
55+
cat src/local.settings.json
56+
```
57+
58+
### 4. Run the Function App Locally
59+
60+
```bash
61+
cd src
62+
func start
63+
```
64+
65+
The Azure Functions runtime will automatically use your Azure credentials via `DefaultAzureCredential`.
66+
67+
## Benefits of This Approach
68+
69+
**No secrets to manage** - No API keys in config files or environment variables
70+
**More secure** - Credentials never leave Azure's identity system
71+
**Production parity** - Local dev works exactly like production
72+
**Automatic role assignment** - Developers get the right permissions when they provision
73+
**Audit trail** - All API calls are tied to specific user identities
74+
75+
## Troubleshooting
76+
77+
### Error: "Access denied" or "Unauthorized"
78+
79+
1. **Check your Azure login**:
80+
```bash
81+
az login
82+
az account show
83+
```
84+
85+
2. **Verify your role assignment**:
86+
```bash
87+
# Get your user principal ID
88+
az ad signed-in-user show --query id -o tsv
89+
90+
# List role assignments (replace <resource-group> and <openai-name>)
91+
az role assignment list --assignee <your-principal-id> \
92+
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<openai-name>
93+
```
94+
95+
3. **If role is missing**, run `azd provision` again or manually assign:
96+
```bash
97+
az role assignment create \
98+
--role "Cognitive Services OpenAI User" \
99+
--assignee <your-email-or-principal-id> \
100+
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<openai-name>
101+
```
102+
103+
### For Additional Developers
104+
105+
If other developers join the project:
106+
107+
**Option 1: They run `azd provision`** (if they have subscription permissions)
108+
- This will automatically assign them the role
109+
110+
**Option 2: Manually assign the role** (if you're the admin)
111+
```bash
112+
az role assignment create \
113+
--role "Cognitive Services OpenAI User" \
114+
--assignee <developer-email> \
115+
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<openai-name>
116+
```
117+
118+
## FAQ
119+
120+
**Q: Do I need an API key for local development?**
121+
A: No! Just make sure you're logged in with `az login`.
122+
123+
**Q: What if I want to use an API key anyway?**
124+
A: You can manually add `AZURE_OPENAI_KEY` to your `local.settings.json`, but it's not recommended. The keyless approach is more secure.
125+
126+
**Q: Does this work in CI/CD pipelines?**
127+
A: Yes! Use a service principal or managed identity for your CI/CD pipeline with the same role assignment.
128+
129+
**Q: What about Cosmos DB access?**
130+
A: Same approach! Your user is assigned `DocumentDB Account Contributor` role during provisioning (see the `cosmosDb` module in `main.bicep`).

infra/app/apim.bicep

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ param skuCapacity int = 1
2424
@allowed(['Enabled', 'Disabled'])
2525
param publicNetworkAccess string = 'Enabled'
2626

27+
@description('App Registration Client ID for MCP')
28+
param appRegistrationClientId string = ''
29+
30+
@description('Azure Entra Tenant ID')
31+
param tenantId string = tenant().tenantId
32+
2733
resource apiManagement 'Microsoft.ApiManagement/service@2023-05-01-preview' = {
2834
name: name
2935
location: location
@@ -50,6 +56,36 @@ resource apiManagement 'Microsoft.ApiManagement/service@2023-05-01-preview' = {
5056
}
5157
}
5258

59+
// Named Value: APIMGateway - APIM Gateway URL
60+
resource namedValueAPIMGateway 'Microsoft.ApiManagement/service/namedValues@2023-05-01-preview' = {
61+
parent: apiManagement
62+
name: 'APIMGateway'
63+
properties: {
64+
displayName: 'APIMGateway'
65+
value: apiManagement.properties.gatewayUrl
66+
}
67+
}
68+
69+
// Named Value: McpClientID - App Registration Client ID
70+
resource namedValueMcpClientID 'Microsoft.ApiManagement/service/namedValues@2023-05-01-preview' = if (!empty(appRegistrationClientId)) {
71+
parent: apiManagement
72+
name: 'McpClientID'
73+
properties: {
74+
displayName: 'McpClientID'
75+
value: appRegistrationClientId
76+
}
77+
}
78+
79+
// Named Value: McpTenantID - Azure Entra Tenant ID
80+
resource namedValueMcpTenantID 'Microsoft.ApiManagement/service/namedValues@2023-05-01-preview' = {
81+
parent: apiManagement
82+
name: 'McpTenantID'
83+
properties: {
84+
displayName: 'McpTenantID'
85+
value: tenantId
86+
}
87+
}
88+
5389
// Outputs
5490
output apiManagementId string = apiManagement.id
5591
output apiManagementName string = apiManagement.name

infra/app/rbac/openai-access.bicep

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ param roleDefinitionId string
77
@description('The principal ID to assign the role to')
88
param principalId string
99

10+
@description('The type of principal (User, Group, or ServicePrincipal)')
11+
@allowed(['User', 'Group', 'ServicePrincipal'])
12+
param principalType string = 'ServicePrincipal'
13+
1014
resource openAIAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {
1115
name: openAIAccountName
1216
}
@@ -17,6 +21,6 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
1721
properties: {
1822
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
1923
principalId: principalId
20-
principalType: 'ServicePrincipal'
24+
principalType: principalType
2125
}
2226
}

infra/app/util/region-selector.bicep

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ func getAdjustedRegion(location string, map object) string =>
33

44
// See https://learn.microsoft.com/azure/ai-services/openai/concepts/models#model-summary-table-and-region-availability
55
var modelRegionMap = {
6+
'text-embedding-ada-002': {
7+
// Widely available in most Azure OpenAI regions
8+
supportedRegions: [
9+
'australiaeast', 'brazilsouth', 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'japaneast'
10+
'northcentralus', 'norwayeast', 'southcentralus', 'swedencentral', 'switzerlandnorth', 'uksouth', 'westeurope', 'westus'
11+
]
12+
overrides: {
13+
westus2: 'westus'
14+
westus3: 'westus'
15+
}
16+
default: 'eastus'
17+
}
618
'text-embedding-3-small': {
719
// Currently supported regions:
820
// Australia East, Canada East, East US, East US 2, Japan East, Switzerland North, UAE North, West US

infra/main.bicep

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,19 @@ module openaiRoleAssignment 'app/rbac/openai-access.bicep' = {
170170
openAIAccountName: openai.outputs.aiServicesName
171171
roleDefinitionId: CognitiveServicesOpenAIUser
172172
principalId: apiUserAssignedIdentity.outputs.principalId
173+
principalType: 'ServicePrincipal'
174+
}
175+
}
176+
177+
// Assign Cognitive Services OpenAI User role to the developer (for local development)
178+
module openaiRoleAssignmentDeveloper 'app/rbac/openai-access.bicep' = if (!empty(principalId)) {
179+
name: 'openaiRoleAssignmentDeveloper'
180+
scope: rg
181+
params: {
182+
openAIAccountName: openai.outputs.aiServicesName
183+
roleDefinitionId: CognitiveServicesOpenAIUser
184+
principalId: principalId
185+
principalType: 'User'
173186
}
174187
}
175188

@@ -237,7 +250,11 @@ module apim './app/apim.bicep' = {
237250
publisherEmail: apimPublisherEmail
238251
skuName: 'BasicV2'
239252
skuCapacity: 1
253+
appRegistrationClientId: appRegistrationClientId
240254
}
255+
dependsOn: [
256+
appRegistration
257+
]
241258
}
242259

243260
// App Registration for MCP Server

scripts/generate-settings.ps1

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
# Get values from azd environment
44
Write-Host "Getting environment values from azd..."
5-
$azdValues = azd env get-values
6-
$COSMOS_ENDPOINT = ($azdValues | Select-String 'COSMOS_ENDPOINT="(.*?)"').Matches.Groups[1].Value
7-
$PROJECT_CONNECTION_STRING = ($azdValues | Select-String 'PROJECT_CONNECTION_STRING="(.*?)"').Matches.Groups[1].Value
8-
$AZURE_OPENAI_ENDPOINT = ($azdValues | Select-String 'AZURE_OPENAI_ENDPOINT="(.*?)"').Matches.Groups[1].Value
9-
$AZURE_OPENAI_KEY = ($azdValues | Select-String 'AZURE_OPENAI_KEY="(.*?)"').Matches.Groups[1].Value
10-
$AZUREWEBJOBSSTORAGE = ($azdValues | Select-String 'AZUREWEBJOBSSTORAGE="(.*?)"').Matches.Groups[1].Value
5+
$envValues = azd env get-values | Out-String
6+
$cosmosEndpoint = ($envValues | Select-String 'COSMOS_ENDPOINT="([^"]*)"').Matches.Groups[1].Value
7+
$azureOpenAIEndpoint = ($envValues | Select-String 'AZURE_OPENAI_ENDPOINT="([^"]*)"').Matches.Groups[1].Value
8+
$azureWebJobsStorage = ($envValues | Select-String 'AZUREWEBJOBSSTORAGE="([^"]*)"').Matches.Groups[1].Value
119

12-
# Create the JSON content
13-
$jsonContent = @"
10+
# Create or update local.settings.json
11+
Write-Host "Generating local.settings.json in src directory..."
12+
$settingsJson = @"
1413
{
1514
"IsEncrypted": false,
1615
"Values": {
@@ -22,16 +21,17 @@ $jsonContent = @"
2221
"BLOB_CONTAINER_NAME": "snippet-backups",
2322
"EMBEDDING_MODEL_DEPLOYMENT_NAME": "text-embedding-3-small",
2423
"AGENTS_MODEL_DEPLOYMENT_NAME": "gpt-4o-mini",
25-
"COSMOS_ENDPOINT": "$COSMOS_ENDPOINT",
26-
"PROJECT_CONNECTION_STRING": "$PROJECT_CONNECTION_STRING",
27-
"AZURE_OPENAI_ENDPOINT": "$AZURE_OPENAI_ENDPOINT",
28-
"AZURE_OPENAI_KEY": "$AZURE_OPENAI_KEY"
24+
"COSMOS_ENDPOINT": "$cosmosEndpoint",
25+
"AZURE_OPENAI_ENDPOINT": "$azureOpenAIEndpoint"
2926
}
3027
}
3128
"@
3229

33-
# Write content to local.settings.json
34-
$settingsPath = Join-Path (Get-Location) "src" "local.settings.json"
35-
$jsonContent | Out-File -FilePath $settingsPath -Encoding utf8
36-
37-
Write-Host "local.settings.json generated successfully in src directory!"
30+
$settingsJson | Out-File -FilePath "src/local.settings.json" -Encoding utf8
31+
Write-Host ""
32+
Write-Host "✅ local.settings.json generated successfully!" -ForegroundColor Green
33+
Write-Host ""
34+
Write-Host "📝 Note: This configuration uses Azure credential authentication (no API key)." -ForegroundColor Cyan
35+
Write-Host " - Make sure you're logged in: az login" -ForegroundColor Cyan
36+
Write-Host " - You should have been automatically assigned the 'Cognitive Services OpenAI User' role" -ForegroundColor Cyan
37+
Write-Host " - If you get authentication errors, verify your role assignment in the Azure Portal" -ForegroundColor Cyan

scripts/generate-settings.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
# Get values from azd environment
44
echo "Getting environment values from azd..."
55
COSMOS_ENDPOINT=$(azd env get-values | grep COSMOS_ENDPOINT | cut -d'"' -f2)
6-
PROJECT_CONNECTION_STRING=$(azd env get-values | grep PROJECT_CONNECTION_STRING | cut -d'"' -f2)
76
AZURE_OPENAI_ENDPOINT=$(azd env get-values | grep AZURE_OPENAI_ENDPOINT | cut -d'"' -f2)
8-
AZURE_OPENAI_KEY=$(azd env get-values | grep AZURE_OPENAI_KEY | cut -d'"' -f2)
97
AZUREWEBJOBSSTORAGE=$(azd env get-values | grep AZUREWEBJOBSSTORAGE | cut -d'"' -f2)
108

119
# Create or update local.settings.json
@@ -23,11 +21,15 @@ cat > src/local.settings.json << EOF
2321
"EMBEDDING_MODEL_DEPLOYMENT_NAME": "text-embedding-3-small",
2422
"AGENTS_MODEL_DEPLOYMENT_NAME": "gpt-4o-mini",
2523
"COSMOS_ENDPOINT": "$COSMOS_ENDPOINT",
26-
"PROJECT_CONNECTION_STRING": "$PROJECT_CONNECTION_STRING",
27-
"AZURE_OPENAI_ENDPOINT": "$AZURE_OPENAI_ENDPOINT",
28-
"AZURE_OPENAI_KEY": "$AZURE_OPENAI_KEY"
24+
"AZURE_OPENAI_ENDPOINT": "$AZURE_OPENAI_ENDPOINT"
2925
}
3026
}
3127
EOF
3228

33-
echo "local.settings.json generated successfully in src directory!"
29+
echo ""
30+
echo "✅ local.settings.json generated successfully!"
31+
echo ""
32+
echo "📝 Note: This configuration uses Azure credential authentication (no API key)."
33+
echo " - Make sure you're logged in: az login"
34+
echo " - You should have been automatically assigned the 'Cognitive Services OpenAI User' role"
35+
echo " - If you get authentication errors, verify your role assignment in the Azure Portal"

src/local.settings.example.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
"EMBEDDING_MODEL_DEPLOYMENT_NAME": "text-embedding-3-small",
1111
"AGENTS_MODEL_DEPLOYMENT_NAME": "gpt-4o",
1212
"COSMOS_ENDPOINT": "https://<cosmos-account-name>.documents.azure.com:443/",
13-
"PROJECT_CONNECTION_STRING": "<region>.api.azureml.ms;<workspace-id>;<project-name>;<project-name>",
14-
"AZURE_OPENAI_ENDPOINT": "https://<service-id>.openai.azure.com/",
15-
"AZURE_OPENAI_KEY": "<your-azure-openai-key>"
13+
"AZURE_OPENAI_ENDPOINT": "https://<service-id>.openai.azure.com/"
1614
}
1715
}

0 commit comments

Comments
 (0)