Skip to content

Commit 125df35

Browse files
committed
docs: Add decision record and resolution for APIM Named Values pattern
- Created decision record for choosing Named Values over string replacement - Documented GitHub Actions secret passing pattern in common-resolutions - Explains workflow template enhancement for tenantId injection - References issue #152 and PR #154
1 parent c0f809d commit 125df35

File tree

2 files changed

+321
-0
lines changed

2 files changed

+321
-0
lines changed

.specify/memory/common-resolutions.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,106 @@ internal class Program
492492

493493
---
494494

495+
## GitHub Actions - Passing Secrets to Bicep Parameters
496+
497+
### Issue: Secrets Cannot Be Passed Through Job Outputs or Workflow Inputs
498+
499+
**Symptoms**:
500+
- Bicep parameter receives empty string instead of secret value
501+
- Double-slash in URLs due to missing tenant ID: `https://login.microsoftonline.com//v2.0/`
502+
- Deployment fails with validation errors about malformed URLs
503+
- Using `outputs: { tenantId: ${{ secrets.AZURE_TENANT_ID }} }` in job results in empty value
504+
505+
**Root Cause**:
506+
GitHub Actions **secrets context is not available** in:
507+
1. Job outputs (`outputs:` block)
508+
2. Reusable workflow inputs (`with:` block)
509+
510+
Secrets can only be accessed in:
511+
- `secrets:` block when calling reusable workflows
512+
- Direct step commands within the workflow
513+
514+
**Solution**:
515+
Inject secrets into Bicep parameters within the **workflow template** using a preparation step:
516+
517+
**Workflow Template Pattern** (`template-bicep-deploy.yml`, `template-bicep-validate.yml`, `template-bicep-whatif.yml`):
518+
519+
```yaml
520+
on:
521+
workflow_call:
522+
inputs:
523+
parameters:
524+
required: false
525+
type: string
526+
secrets:
527+
tenant-id:
528+
required: true
529+
530+
jobs:
531+
deploy:
532+
steps:
533+
- uses: azure/login@v2
534+
with:
535+
tenant-id: ${{ secrets.tenant-id }}
536+
537+
- name: Prepare Parameters with Tenant ID
538+
id: prepare-params
539+
shell: bash
540+
run: |
541+
if [ -n "${{ inputs.parameters }}" ]; then
542+
# Merge user parameters with tenantId
543+
PARAMS=$(echo '${{ inputs.parameters }}' | jq -c '. + {"tenantId": "${{ secrets.tenant-id }}"}')
544+
else
545+
# Only tenantId
546+
PARAMS='{"tenantId": "${{ secrets.tenant-id }}"}'
547+
fi
548+
echo "params=$PARAMS" >> "$GITHUB_OUTPUT"
549+
550+
- uses: azure/bicep-deploy@v2
551+
with:
552+
parameters: ${{ steps.prepare-params.outputs.params }}
553+
```
554+
555+
**Calling Workflow** (API deployment workflows):
556+
557+
```yaml
558+
validate:
559+
uses: willvelida/biotrackr/.github/workflows/template-bicep-validate.yml@main
560+
with:
561+
template-file: './infra/apps/weight-api/main.bicep'
562+
parameters-file: ./infra/apps/weight-api/main.dev.bicepparam
563+
parameters: '{"imageName": "${{ needs.retrieve-container-image-dev.outputs.loginServer }}/biotrackr-weight-api:${{ github.sha }}"}'
564+
secrets:
565+
tenant-id: ${{ secrets.AZURE_TENANT_ID }} # ✅ Correct - pass via secrets block
566+
```
567+
568+
**❌ Wrong** (secrets in job outputs):
569+
```yaml
570+
retrieve-container-image-dev:
571+
outputs:
572+
tenantId: ${{ secrets.AZURE_TENANT_ID }} # Empty string!
573+
```
574+
575+
**❌ Wrong** (secrets in workflow inputs):
576+
```yaml
577+
validate:
578+
with:
579+
tenantId: ${{ secrets.AZURE_TENANT_ID }} # Not allowed in 'with' block
580+
```
581+
582+
**Resolution History**:
583+
- Fixed in workflow templates (commit c0f809d, 2025-11-12)
584+
- Removed tenantId from job outputs in all API workflows
585+
- Documented in `docs/decision-records/2025-11-12-apim-named-values-for-jwt-config.md`
586+
587+
**Prevention**:
588+
- Always pass secrets through the `secrets:` block of reusable workflows
589+
- Use `jq` in workflow templates to merge secrets into parameters JSON
590+
- Never try to pass secrets through job outputs or workflow `with:` inputs
591+
- Test with actual secret values (not empty strings in .bicepparam files)
592+
593+
---
594+
495595
## Notes
496596

497597
- Keep this document updated as new patterns emerge
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Decision Record: APIM Named Values for JWT Configuration
2+
3+
- **Status**: Accepted
4+
- **Deciders**: Development Team
5+
- **Date**: 12 November 2025
6+
- **Related Issue**: #152 - Enable APIM Managed Identity Authentication
7+
8+
## Context
9+
10+
We need to configure JWT validation in Azure API Management (APIM) for all APIs (Weight, Activity, Sleep, Food) to support Managed Identity authentication from the Blazor UI. The JWT validation requires three configuration values:
11+
12+
1. **OpenID Configuration URL** - `https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration`
13+
2. **JWT Audience** - `https://management.azure.com/` (or custom App ID URI)
14+
3. **JWT Issuer** - `https://login.microsoftonline.com/{tenantId}/v2.0`
15+
16+
We evaluated two approaches for managing these configuration values:
17+
18+
### Approach 1: String Replacement in Policy Files (Initial Implementation)
19+
Use Bicep's `replace()` function to inject values into XML policy files during deployment:
20+
21+
```bicep
22+
resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2024-06-01-preview' = {
23+
name: 'policy'
24+
parent: api
25+
properties: {
26+
format: 'rawxml'
27+
value: replace(
28+
replace(
29+
replace(
30+
loadTextContent('policy-jwt-auth.xml'),
31+
'{{OPENID_CONFIG_URL}}', openidConfigUrl
32+
),
33+
'{{JWT_AUDIENCE}}', jwtAudience
34+
),
35+
'{{JWT_ISSUER}}', jwtIssuer
36+
)
37+
}
38+
}
39+
```
40+
41+
**Issues encountered:**
42+
- Empty `tenantId` parameter caused double-slash in URLs: `https://login.microsoftonline.com//v2.0/`
43+
- GitHub Actions secrets couldn't be passed through job outputs
44+
- Complex nested `replace()` calls reduced readability
45+
- Required Bicep linter suppressions (`#disable-next-line prefer-interpolation`)
46+
- No runtime visibility into configuration values
47+
- Couldn't update values without Bicep redeployment
48+
49+
### Approach 2: APIM Named Values (Chosen Solution)
50+
Use APIM's native Named Values feature to store configuration centrally and reference them in policies:
51+
52+
```bicep
53+
// Create Named Values in APIM
54+
resource openidConfigUrlNamedValue 'Microsoft.ApiManagement/service/namedValues@2024-06-01-preview' = {
55+
name: 'openid-config-url'
56+
parent: apim
57+
properties: {
58+
value: '${environment().authentication.loginEndpoint}${tenantId}/v2.0/.well-known/openid-configuration'
59+
}
60+
}
61+
62+
// Reference in policy XML
63+
resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2024-06-01-preview' = {
64+
name: 'policy'
65+
parent: api
66+
properties: {
67+
format: 'rawxml'
68+
value: loadTextContent('policy-jwt-auth.xml') // No string replacement needed
69+
}
70+
}
71+
```
72+
73+
Policy XML references Named Values using APIM's native syntax:
74+
```xml
75+
<validate-jwt header-name="Authorization">
76+
<openid-config url="{{openid-config-url}}" />
77+
<audiences>
78+
<audience>{{jwt-audience}}</audience>
79+
</audiences>
80+
<issuers>
81+
<issuer>{{jwt-issuer}}</issuer>
82+
</issuers>
83+
</validate-jwt>
84+
```
85+
86+
## Decision
87+
88+
**We will use APIM Named Values to manage JWT configuration** instead of Bicep string replacement.
89+
90+
## Rationale
91+
92+
### Technical Benefits
93+
94+
1. **APIM-Native Approach**
95+
- Named Values are a first-class APIM feature designed for configuration management
96+
- Uses `{{named-value}}` syntax that APIM resolves at runtime
97+
- No Bicep workarounds or linter suppressions needed
98+
99+
2. **Runtime Flexibility**
100+
- Named Values can be updated through Azure Portal without Bicep redeployment
101+
- Useful for troubleshooting and quick configuration changes
102+
- Values visible in APIM Portal for debugging
103+
104+
3. **Cleaner Code**
105+
- Eliminates nested `replace()` function calls
106+
- Policy XML files contain readable `{{named-value}}` references
107+
- Bicep templates are more maintainable
108+
109+
4. **Better Secret Handling**
110+
- Named Values support secret storage (though not used in this implementation)
111+
- Can be sourced from Key Vault if needed
112+
- Easier to rotate secrets without code changes
113+
114+
5. **Centralized Configuration**
115+
- Single source of truth for JWT config in APIM
116+
- All APIs reference the same Named Values
117+
- Consistent configuration across environments
118+
119+
### Workflow Template Enhancement
120+
121+
To pass `tenantId` from GitHub Actions secrets to Bicep, we enhanced workflow templates with a parameter preparation step:
122+
123+
```yaml
124+
- name: Prepare Parameters with Tenant ID
125+
id: prepare-params
126+
shell: bash
127+
run: |
128+
if [ -n "${{ inputs.parameters }}" ]; then
129+
# Merge user parameters with tenantId
130+
PARAMS=$(echo '${{ inputs.parameters }}' | jq -c '. + {"tenantId": "${{ secrets.tenant-id }}"}')
131+
else
132+
# Only tenantId
133+
PARAMS='{"tenantId": "${{ secrets.tenant-id }}"}'
134+
fi
135+
echo "params=$PARAMS" >> "$GITHUB_OUTPUT"
136+
137+
- uses: azure/bicep-deploy@v2
138+
with:
139+
parameters: ${{ steps.prepare-params.outputs.params }}
140+
```
141+
142+
This solved the "double-slash" issue by ensuring `tenantId` is always populated from secrets.
143+
144+
## Consequences
145+
146+
### Positive
147+
- **Simpler Bicep code** - Removed nested `replace()` calls and linter suppressions
148+
- **Better debugging** - Named Values visible in Azure Portal
149+
- **Runtime updates** - Can change values without redeployment
150+
- **Multi-cloud ready** - Uses `environment().authentication.loginEndpoint` for Azure environments
151+
- **Consistent pattern** - All 4 APIs use the same approach
152+
153+
### Negative
154+
- **Additional resources** - Creates 3 Named Values per APIM instance (minimal cost impact)
155+
- **Two-step deployment** - Named Values must exist before policies can reference them (handled by Bicep module dependencies)
156+
157+
### Neutral
158+
- **Learning curve** - Team needs to understand Named Values feature
159+
- **Template updates** - Workflow templates now inject `tenantId` from secrets (one-time change)
160+
161+
## Implementation Details
162+
163+
### Module Structure
164+
Created `infra/modules/apim/apim-named-values.bicep`:
165+
- Conditionally creates Named Values when `enableManagedIdentityAuth = true`
166+
- Uses `environment().authentication.loginEndpoint` for multi-cloud support
167+
- Returns Named Value names as outputs (for dependency management)
168+
169+
### Affected Files
170+
- **Created**: `infra/modules/apim/apim-named-values.bicep`
171+
- **Updated**: All 4 API Bicep files (`infra/apps/*/main.bicep`)
172+
- **Updated**: All 4 policy XML files (`infra/apps/*/policy-jwt-auth.xml`)
173+
- **Updated**: 3 workflow templates (`template-bicep-validate.yml`, `template-bicep-whatif.yml`, `template-bicep-deploy.yml`)
174+
- **Updated**: All 4 API workflows (`deploy-*-api.yml`)
175+
176+
### Validation
177+
```bash
178+
# Verify Named Values created
179+
az rest --method get \
180+
--url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim}/namedValues?api-version=2024-06-01-preview" \
181+
--query "value[?contains(name, 'jwt') || contains(name, 'openid')]"
182+
183+
# Test JWT authentication
184+
TOKEN=$(az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv)
185+
curl -H "Authorization: Bearer $TOKEN" https://api-biotrackr-dev.azure-api.net/weight/api/weight
186+
```
187+
188+
## Alternatives Considered
189+
190+
### Alternative 1: Keep String Replacement with Better Secret Handling
191+
- **Rejected**: Still requires complex `replace()` calls and linter suppressions
192+
- Doesn't solve runtime visibility or update flexibility issues
193+
194+
### Alternative 2: Policy Fragments
195+
- **Considered**: APIM Policy Fragments for reusable policy sections
196+
- **Decision**: Named Values are simpler for configuration values (vs. entire policy blocks)
197+
- Policy Fragments better suited for common policy logic, not configuration
198+
199+
### Alternative 3: Azure Key Vault Integration
200+
- **Future Enhancement**: Named Values can reference Key Vault secrets
201+
- Not needed for current implementation (JWT config is not sensitive)
202+
- Could be added later for truly secret values (API keys, connection strings)
203+
204+
## Follow-up Actions
205+
206+
- [x] Create Named Values module (`apim-named-values.bicep`)
207+
- [x] Update all 4 API Bicep files to use module
208+
- [x] Update all 4 policy XML files with `{{named-value}}` syntax
209+
- [x] Enhance workflow templates with parameter preparation
210+
- [x] Test deployment and JWT validation
211+
- [x] Verify Named Values in Azure Portal
212+
- [ ] Apply same pattern to future APIs (Food, Auth services)
213+
- [ ] Document Named Values pattern in project README
214+
215+
## References
216+
- [APIM Named Values Documentation](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-properties)
217+
- [APIM Policy Expressions](https://learn.microsoft.com/en-us/azure/api-management/api-management-policy-expressions)
218+
- [validate-jwt Policy Reference](https://learn.microsoft.com/en-us/azure/api-management/validate-jwt-policy)
219+
- [GitHub Actions: Reusable Workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows)
220+
- Issue #152: Enable APIM Managed Identity Authentication
221+
- PR #154: feat: Add JWT validation to APIM for managed identity authentication

0 commit comments

Comments
 (0)