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