Skip to content

feat: Add JWT validation to APIM for managed identity authentication#154

Merged
willvelida merged 6 commits intomainfrom
013-apim-managed-identity
Nov 12, 2025
Merged

feat: Add JWT validation to APIM for managed identity authentication#154
willvelida merged 6 commits intomainfrom
013-apim-managed-identity

Conversation

@willvelida
Copy link
Owner

@willvelida willvelida commented Nov 12, 2025

Summary

Implements JWT validation in Azure API Management using APIM Named Values pattern for all 4 APIs (Weight, Activity, Sleep, Food). This enables Managed Identity authentication for the future Blazor UI while maintaining backward compatibility with subscription key authentication.

Changes

Infrastructure

  • New Module: infra/modules/apim/apim-named-values.bicep

    • Creates 3 APIM Named Values: openid-config-url, jwt-audience, jwt-issuer
    • Conditionally deployed when enableManagedIdentityAuth = true
    • Uses environment().authentication.loginEndpoint for multi-cloud support
  • Updated API Bicep Files (Weight, Activity, Sleep, Food):

    • Added module call to apim-named-values
    • Removed string replacement logic (replace() calls)
    • Simplified policy resources to use loadTextContent() only
    • Removed unused variables and linter suppressions
  • Updated Policy XML Files (all 4 APIs):

    • Changed placeholders to APIM Native Value syntax: {{openid-config-url}}, {{jwt-audience}}, {{jwt-issuer}}
    • Maintains <choose> logic: JWT validation for Bearer tokens, subscription key fallback for others
  • Updated Parameter Files (all 4 APIs):

    • Added enableManagedIdentityAuth = true
    • Added tenantId = '' (empty default, overridden by workflow)

CI/CD Workflows

  • Enhanced Workflow Templates:

    • template-bicep-validate.yml
    • template-bicep-whatif.yml
    • template-bicep-deploy.yml
    • Added parameter preparation step to inject tenantId from secrets using jq
    • Solves GitHub Actions limitation: secrets can't be passed through job outputs
  • Updated API Workflows (all 4):

    • Removed tenantId from job outputs
    • Removed tenantId from parameters JSON
    • Templates now handle tenantId injection automatically

Documentation

  • Decision Record: docs/decision-records/2025-11-12-apim-named-values-for-jwt-config.md

    • Documents why Named Values chosen over string replacement
    • Explains workflow template enhancement pattern
  • Common Resolutions: .specify/memory/common-resolutions.md

    • Added pattern for passing secrets to Bicep via workflow templates
    • Prevents future "double-slash URL" issues

Testing Results ✅

Deployment Status

  • ✅ All 4 API workflows passed deployment
  • ✅ APIM Named Values created successfully
  • ✅ JWT validation policies deployed

APIM Named Values Verification

Name               Value
-----------------  ------------------------------------------------------------------------------------------------------------
jwt-audience       https://management.core.windows.net/
jwt-issuer         https://login.microsoftonline.com/e60bb76c-8fda-4ed4-a354-4836e3bfcbc3/v2.0
openid-config-url  https://login.microsoftonline.com/e60bb76c-8fda-4ed4-a354-4836e3bfcbc3/v2.0/.well-known/openid-configuration

No double slashes - tenantId injection working correctly!

JWT Authentication Testing

Test Setup:

# Acquired JWT token
TOKEN=$(az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv)

# Tested all 4 APIs with Bearer token
curl -H "Authorization: Bearer $TOKEN" https://api-biotrackr-dev.azure-api.net/{api}/api/{resource}

Results:

API Endpoint Auth Method Response Status
Weight /weight/api/weight JWT Bearer 404 Resource Not Found ✅ Pass
Activity /activity/api/activity JWT Bearer 404 Resource Not Found ✅ Pass
Sleep /sleep/api/sleep JWT Bearer 404 Resource Not Found ✅ Pass
Food /food/api/food JWT Bearer 404 Resource Not Found ✅ Pass

Analysis:

  • All APIs returned 404 (Resource Not Found) instead of 401 (Unauthorized)
  • This confirms JWT authentication is working correctly
  • 404 response means the request passed authentication and reached the backend API
  • No data exists yet, which is expected for fresh deployment

Backward Compatibility

  • ✅ No subscription key requirement enforced (backward compatible by default in Consumption tier)
  • ✅ Policy has <otherwise> clause for subscription key fallback
  • ✅ Existing subscription-based clients will continue to work

Deployment Notes

  • Branch References: API workflows temporarily reference @013-apim-managed-identity to test updated templates
    • Need to revert to @main after merge
  • Empty tenantId: Parameter files have tenantId = '' - actual value injected by workflows
  • Conditional Deployment: Named Values only created when enableManagedIdentityAuth = true

Implementation Highlights

Why Named Values Instead of String Replacement?

  1. APIM-Native: Uses built-in feature designed for configuration management
  2. Runtime Updates: Can change values in Portal without Bicep redeployment
  3. Better Debugging: Values visible in APIM Portal
  4. Cleaner Code: Eliminates nested replace() calls and linter suppressions
  5. Secret Support: Can integrate with Key Vault for sensitive values (future)

Workflow Secret Passing Pattern

GitHub Actions doesn't allow secrets in reusable workflow with: blocks or job outputs:. Solution:

# Template injects secret into parameters
- name: Prepare Parameters with Tenant ID
  run: |
    PARAMS=$(echo '${{ inputs.parameters }}' | jq -c '. + {"tenantId": "${{ secrets.tenant-id }}"}')
    echo "params=$PARAMS" >> "$GITHUB_OUTPUT"

Checklist

  • Bicep modules created/updated
  • Policy XML files updated
  • Parameter files updated
  • Workflow templates enhanced
  • API workflows updated
  • All deployments successful
  • JWT authentication tested
  • Named Values verified
  • Decision record created
  • Common resolutions updated
  • Revert workflow branch references to @main
  • Update issue Enable APIM Managed Identity Authentication for UI Consumer #152 acceptance criteria

Related

- Added validate-jwt policies to all 4 backend APIs (Weight, Activity, Sleep, Food)
- Implemented choose policy for dual authentication (JWT OR subscription key)
- Uses Azure AD tenant OpenID config for token validation
- Parameterized tenantId and jwtAudience using environment() function
- Maintains backward compatibility with subscription keys
- Created decision record documenting architecture and rationale
- Updated README with APIM authentication capability

Related to #152
- Replace inline policy XML with loadTextContent() in Bicep files
- Add policy-jwt-auth.xml with JWT validation logic
- Add policy-subscription-key.xml for backward compatibility
- Use replace() to substitute placeholders with Bicep variables
- Improves maintainability and testability of policies

All 4 APIs refactored: Weight, Activity, Sleep, Food
- Create apim-named-values.bicep module for centralized Named Values
- Deploy Named Values conditionally based on enableManagedIdentityAuth
- Update policy XML to use {{openid-config-url}}, {{jwt-audience}}, {{jwt-issuer}}
- Remove string replace() calls from Bicep policy resources
- Remove unused openidConfigUrl and jwtIssuer variables
- Remove Bicep linter suppressions (no longer needed)

Benefits:
- APIM-native approach with runtime flexibility
- Cleaner Bicep files (no triple replace() calls)
- Values visible in Azure Portal for troubleshooting
- Can update Named Values without redeploying Bicep
- Add tenantId parameter to all 4 API parameter files (default empty string)
- Enable managed identity authentication (enableManagedIdentityAuth=true)
- Pass tenantId from GitHub Actions AZURE_TENANT_ID secret via workflow
- Add tenantId output to retrieve-container-image-dev jobs
- Update validate, preview, and deploy steps with tenantId parameter

Workflow will override empty tenantId with actual value from secret
- Add parameter preparation step in workflow templates to merge tenantId from secrets
- Remove tenantId from job outputs (secrets can't be passed through outputs)
- Use jq to merge user parameters with tenantId before deployment
- Fixes double-slash in OpenID config URL (https://login.microsoftonline.com//v2.0)
- 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
@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
Biotrackr.Activity.Api 80% 86%
Summary 80% (214 / 269) 86% (24 / 28)

Minimum allowed line rate is 70%

@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
Biotrackr.Weight.Api 75% 79%
Summary 75% (210 / 280) 79% (22 / 28)

Minimum allowed line rate is 70%

@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
Biotrackr.Food.Api 77% 71%
Summary 77% (224 / 291) 71% (20 / 28)

Minimum allowed line rate is 70%

@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
Biotrackr.Sleep.Api 87% 92%
Summary 87% (215 / 247) 92% (22 / 24)

Minimum allowed line rate is 80%

@willvelida willvelida merged commit e69ef8a into main Nov 12, 2025
47 checks passed
@willvelida willvelida deleted the 013-apim-managed-identity branch November 12, 2025 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant