Skip to content

azure-oidc

helps configure and implement GitHub OIDC authentication with Azure for secure, keyless access from GitHub Actions to Azure resources

IDE:
claude
codex
vscode
Version:
0.0.0

GitHub OIDC Authentication for Azure

Overview

OpenID Connect (OIDC) allows your GitHub Actions workflows to access resources in Microsoft Azure without needing to store Azure credentials as long-lived GitHub secrets. This eliminates the security risks associated with static credentials and provides temporary, scoped access tokens through Azure Active Directory (Azure AD) / Microsoft Entra ID.

Core Concepts

What is OIDC?

  • Federated Identity: Azure trusts GitHub's OIDC provider to authenticate workflows
  • Temporary Tokens: Short-lived tokens replace static credentials
  • Scoped Access: Fine-grained permissions based on repository, branch, and environment
  • Security: No secrets stored in GitHub; tokens are generated on-demand

What is Federated Identity Credential?

Azure's Federated Identity Credential feature allows external identity providers (like GitHub Actions) to access Azure resources without managing secrets or certificates. This is Azure's implementation of workload identity federation.

Benefits:

  • Improved Security: Eliminates the need to store and rotate service principal secrets
  • Automatic Token Management: Azure AD handles token lifecycle automatically
  • Simplified Management: Centralized identity and access management through Azure AD
  • Fine-grained Access Control: Azure RBAC provides granular permissions

Key Components

  1. Azure AD App Registration: Application identity in Azure Active Directory
  2. Service Principal: Enterprise application that represents the GitHub Actions identity
  3. Federated Identity Credential: Configuration that trusts GitHub's OIDC provider
  4. Azure RBAC: Role assignments that define what actions the identity can perform
  5. Token Exchange: GitHub token is exchanged for Azure AD access token

Authentication Flow

  1. GitHub Actions workflow requests OIDC token from https://token.actions.githubusercontent.com
  2. GitHub issues JWT token with workflow identity claims
  3. Token is sent to Azure AD for validation
  4. Azure AD validates token against Federated Identity Credential configuration
  5. Azure AD issues access token with appropriate permissions
  6. Workflow uses access token to access Azure resources

Setup Methods

Method 1: Manual Configuration via Azure Portal & CLI

Prerequisites

  • Azure subscription with appropriate permissions
  • Azure CLI installed and configured
  • GitHub repository for Actions workflows
  • Owner or Application Administrator role in Azure AD

Step 1: Authenticate with Azure

# Login to Azure
az login

# Set your subscription
az account set --subscription "Your-Subscription-Name-or-ID"

# Verify current subscription
az account show

Step 2: Create Azure AD App Registration

# Create App Registration
APP_NAME="github-actions-app"
az ad app create --display-name "$APP_NAME"

# Get the Application (client) ID
APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv)
echo "Application ID: $APP_ID"

# Get the Tenant ID
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "Tenant ID: $TENANT_ID"

Step 3: Create Service Principal

# Create service principal for the app
az ad sp create --id $APP_ID

# Get the Service Principal Object ID
SP_OBJECT_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
echo "Service Principal Object ID: $SP_OBJECT_ID"

Step 4: Configure Federated Identity Credential

# Define your GitHub organization, repository, and environment
GITHUB_ORG="your-org"
GITHUB_REPO="your-repo"
ENVIRONMENT="production"  # Optional: for environment-specific access

# For main branch access
cat <<EOF > federated-credential.json
{
  "name": "github-actions-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/main",
  "description": "GitHub Actions access for main branch",
  "audiences": [
    "api://AzureADTokenExchange"
  ]
}
EOF

# Create the federated credential
az ad app federated-credential create \
  --id $APP_ID \
  --parameters @federated-credential.json

# For environment-based access (alternative)
cat <<EOF > federated-credential-env.json
{
  "name": "github-actions-${ENVIRONMENT}",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:${ENVIRONMENT}",
  "description": "GitHub Actions access for ${ENVIRONMENT} environment",
  "audiences": [
    "api://AzureADTokenExchange"
  ]
}
EOF

az ad app federated-credential create \
  --id $APP_ID \
  --parameters @federated-credential-env.json

# For pull request access (optional)
cat <<EOF > federated-credential-pr.json
{
  "name": "github-actions-pr",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:pull_request",
  "description": "GitHub Actions access for pull requests",
  "audiences": [
    "api://AzureADTokenExchange"
  ]
}
EOF

az ad app federated-credential create \
  --id $APP_ID \
  --parameters @federated-credential-pr.json

Step 5: Assign Azure RBAC Roles

# Get your subscription ID
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

# Assign Contributor role at subscription level (adjust scope as needed)
az role assignment create \
  --assignee $APP_ID \
  --role "Contributor" \
  --scope "/subscriptions/$SUBSCRIPTION_ID"

# For resource group scope (more restrictive)
RESOURCE_GROUP="your-resource-group"
az role assignment create \
  --assignee $APP_ID \
  --role "Contributor" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"

# For specific resource scope (most restrictive)
RESOURCE_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/yourstorageaccount"
az role assignment create \
  --assignee $APP_ID \
  --role "Storage Blob Data Contributor" \
  --scope "$RESOURCE_ID"

Step 6: Verify Configuration

# List federated credentials
az ad app federated-credential list --id $APP_ID

# List role assignments
az role assignment list --assignee $APP_ID --output table

Note the following values for GitHub Actions configuration:

  • Client ID: $APP_ID (Application ID)
  • Tenant ID: $TENANT_ID
  • Subscription ID: $SUBSCRIPTION_ID

Method 2: Terraform Infrastructure as Code

Terraform provides a repeatable, version-controlled way to configure Azure OIDC for multiple projects and environments.

Prerequisites

  • Terraform installed (version >= 1.3.0)
  • Azure CLI authenticated
  • Azure subscription created

Repository Structure

terraform/
├── providers.tf         # Provider configuration
├── main.tf             # App registration and federated credentials
├── rbac.tf             # Role assignments
├── variables.tf        # Input variables
├── outputs.tf          # Output values
└── tfvars/
    ├── dev.tfvars      # Development environment
    ├── staging.tfvars  # Staging environment
    └── prod.tfvars     # Production environment

Key Terraform Resources

1. Provider Configuration (providers.tf)

terraform {
  required_version = ">= 1.3.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.80"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.45"
    }
  }

  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstate"
    container_name       = "tfstate"
    key                  = "github-oidc.tfstate"
  }
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

provider "azuread" {
  tenant_id = var.tenant_id
}

2. App Registration and Federated Credentials (main.tf)

# Data source for current Azure AD configuration
data "azuread_client_config" "current" {}

# Create Azure AD Application
resource "azuread_application" "github_actions" {
  display_name = "${var.app_name}-${var.environment}"
  owners       = [data.azuread_client_config.current.object_id]

  tags = [
    "Environment:${var.environment}",
    "ManagedBy:Terraform",
    "Purpose:GitHubActions"
  ]
}

# Create Service Principal
resource "azuread_service_principal" "github_actions" {
  application_id               = azuread_application.github_actions.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]

  tags = [
    "Environment:${var.environment}",
    "ManagedBy:Terraform"
  ]
}

# Federated Credential for main branch
resource "azuread_application_federated_identity_credential" "main_branch" {
  application_object_id = azuread_application.github_actions.object_id
  display_name          = "github-actions-main-${var.environment}"
  description           = "GitHub Actions access for main branch in ${var.environment}"
  audiences             = ["api://AzureADTokenExchange"]
  issuer                = "https://token.actions.githubusercontent.com"
  subject               = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main"
}

# Federated Credential for develop branch (optional)
resource "azuread_application_federated_identity_credential" "develop_branch" {
  count = var.enable_develop_branch ? 1 : 0

  application_object_id = azuread_application.github_actions.object_id
  display_name          = "github-actions-develop-${var.environment}"
  description           = "GitHub Actions access for develop branch in ${var.environment}"
  audiences             = ["api://AzureADTokenExchange"]
  issuer                = "https://token.actions.githubusercontent.com"
  subject               = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/develop"
}

# Federated Credential for environment-based access
resource "azuread_application_federated_identity_credential" "environment" {
  count = var.enable_environment_credential ? 1 : 0

  application_object_id = azuread_application.github_actions.object_id
  display_name          = "github-actions-env-${var.environment}"
  description           = "GitHub Actions access for ${var.environment} environment"
  audiences             = ["api://AzureADTokenExchange"]
  issuer                = "https://token.actions.githubusercontent.com"
  subject               = "repo:${var.github_org}/${var.github_repo}:environment:${var.environment}"
}

# Federated Credential for pull requests (optional)
resource "azuread_application_federated_identity_credential" "pull_request" {
  count = var.enable_pr_access ? 1 : 0

  application_object_id = azuread_application.github_actions.object_id
  display_name          = "github-actions-pr-${var.environment}"
  description           = "GitHub Actions access for pull requests in ${var.environment}"
  audiences             = ["api://AzureADTokenExchange"]
  issuer                = "https://token.actions.githubusercontent.com"
  subject               = "repo:${var.github_org}/${var.github_repo}:pull_request"
}

3. RBAC Role Assignments (rbac.tf)

# Data source for subscription
data "azurerm_subscription" "current" {}

# Data source for resource group (if scope is resource group)
data "azurerm_resource_group" "target" {
  count = var.rbac_scope == "resource_group" ? 1 : 0
  name  = var.target_resource_group
}

# Built-in role assignments
resource "azurerm_role_assignment" "builtin_roles" {
  for_each = toset(var.builtin_roles)

  scope                = local.rbac_scope
  role_definition_name = each.value
  principal_id         = azuread_service_principal.github_actions.object_id

  depends_on = [azuread_service_principal.github_actions]
}

# Custom role definition (if needed)
resource "azurerm_role_definition" "custom" {
  count = var.create_custom_role ? 1 : 0

  name        = "${var.app_name}-custom-role-${var.environment}"
  scope       = data.azurerm_subscription.current.id
  description = "Custom role for GitHub Actions in ${var.environment}"

  permissions {
    actions = var.custom_role_actions
    not_actions = var.custom_role_not_actions
    data_actions = var.custom_role_data_actions
    not_data_actions = var.custom_role_not_data_actions
  }

  assignable_scopes = [
    local.rbac_scope
  ]
}

# Assign custom role
resource "azurerm_role_assignment" "custom" {
  count = var.create_custom_role ? 1 : 0

  scope              = local.rbac_scope
  role_definition_id = azurerm_role_definition.custom[0].role_definition_resource_id
  principal_id       = azuread_service_principal.github_actions.object_id

  depends_on = [
    azuread_service_principal.github_actions,
    azurerm_role_definition.custom
  ]
}

# Local values for scope determination
locals {
  rbac_scope = var.rbac_scope == "subscription" ? data.azurerm_subscription.current.id : (
    var.rbac_scope == "resource_group" ? data.azurerm_resource_group.target[0].id :
    var.custom_rbac_scope
  )
}

4. Variables (variables.tf)

variable "subscription_id" {
  type        = string
  description = "Azure subscription ID"
}

variable "tenant_id" {
  type        = string
  description = "Azure AD tenant ID"
}

variable "environment" {
  type        = string
  description = "Environment name (dev, staging, prod)"
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "app_name" {
  type        = string
  description = "Base name for the Azure AD application"
  default     = "github-actions"
}

variable "github_org" {
  type        = string
  description = "GitHub organization name"
}

variable "github_repo" {
  type        = string
  description = "GitHub repository name"
}

variable "enable_develop_branch" {
  type        = bool
  description = "Enable federated credential for develop branch"
  default     = false
}

variable "enable_environment_credential" {
  type        = bool
  description = "Enable environment-based federated credential"
  default     = true
}

variable "enable_pr_access" {
  type        = bool
  description = "Enable federated credential for pull requests"
  default     = false
}

variable "rbac_scope" {
  type        = string
  description = "Scope for RBAC assignment: subscription, resource_group, or custom"
  default     = "subscription"
  validation {
    condition     = contains(["subscription", "resource_group", "custom"], var.rbac_scope)
    error_message = "RBAC scope must be subscription, resource_group, or custom."
  }
}

variable "target_resource_group" {
  type        = string
  description = "Target resource group name (required if rbac_scope is resource_group)"
  default     = null
}

variable "custom_rbac_scope" {
  type        = string
  description = "Custom RBAC scope (full resource ID)"
  default     = null
}

variable "builtin_roles" {
  type        = list(string)
  description = "List of built-in Azure roles to assign"
  default     = ["Contributor"]
}

variable "create_custom_role" {
  type        = bool
  description = "Create and assign a custom role"
  default     = false
}

variable "custom_role_actions" {
  type        = list(string)
  description = "Actions allowed in custom role"
  default     = []
}

variable "custom_role_not_actions" {
  type        = list(string)
  description = "Actions denied in custom role"
  default     = []
}

variable "custom_role_data_actions" {
  type        = list(string)
  description = "Data actions allowed in custom role"
  default     = []
}

variable "custom_role_not_data_actions" {
  type        = list(string)
  description = "Data actions denied in custom role"
  default     = []
}

5. Outputs (outputs.tf)

output "application_id" {
  description = "Azure AD Application (Client) ID"
  value       = azuread_application.github_actions.application_id
}

output "tenant_id" {
  description = "Azure AD Tenant ID"
  value       = var.tenant_id
}

output "subscription_id" {
  description = "Azure Subscription ID"
  value       = var.subscription_id
}

output "service_principal_object_id" {
  description = "Service Principal Object ID"
  value       = azuread_service_principal.github_actions.object_id
}

output "federated_credentials" {
  description = "List of federated credential subjects"
  value = {
    main_branch = azuread_application_federated_identity_credential.main_branch.subject
    develop_branch = var.enable_develop_branch ? azuread_application_federated_identity_credential.develop_branch[0].subject : null
    environment = var.enable_environment_credential ? azuread_application_federated_identity_credential.environment[0].subject : null
    pull_request = var.enable_pr_access ? azuread_application_federated_identity_credential.pull_request[0].subject : null
  }
}

output "rbac_scope" {
  description = "RBAC assignment scope"
  value       = local.rbac_scope
}

Example Configuration (tfvars/dev.tfvars)

subscription_id = "12345678-1234-1234-1234-123456789012"
tenant_id       = "87654321-4321-4321-4321-210987654321"
environment     = "dev"
app_name        = "github-actions"
github_org      = "OptumInsight-Platform"
github_repo     = "my-application"

enable_develop_branch         = true
enable_environment_credential = true
enable_pr_access             = true

rbac_scope            = "resource_group"
target_resource_group = "my-app-dev-rg"

builtin_roles = [
  "Contributor",
  "Storage Blob Data Contributor"
]

create_custom_role = false

Deployment Commands

# Navigate to terraform directory
cd terraform

# Initialize Terraform
terraform init

# Validate configuration
terraform validate

# Preview changes
terraform plan -var-file=tfvars/dev.tfvars -out=tfplan

# Apply configuration
terraform apply tfplan

# View outputs
terraform output

# Save outputs for GitHub Actions secrets
terraform output -raw application_id > client_id.txt
terraform output -raw tenant_id > tenant_id.txt
terraform output -raw subscription_id > subscription_id.txt

GitHub Actions Integration

Required Permissions

permissions:
  id-token: write   # Required for OIDC token generation
  contents: read    # Required for repository access

Basic Workflow Example

name: Deploy to Azure
on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: uhg-runner
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Azure Login with OIDC
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Verify authentication
        run: |
          az account show
          az group list --output table

Advanced Workflow with Multiple Environments

name: Multi-Environment Deployment
on:
  push:
    branches:
      - main
      - develop

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: uhg-runner
    strategy:
      matrix:
        environment: [dev, staging, prod]
        include:
          - environment: dev
            azure-client-id: ${{ secrets.AZURE_CLIENT_ID_DEV }}
            azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_DEV }}
            condition: github.ref == 'refs/heads/develop'
          - environment: staging
            azure-client-id: ${{ secrets.AZURE_CLIENT_ID_STAGING }}
            azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_STAGING }}
            condition: github.ref == 'refs/heads/main'
          - environment: prod
            azure-client-id: ${{ secrets.AZURE_CLIENT_ID_PROD }}
            azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_PROD }}
            condition: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'

    if: ${{ matrix.condition }}
    environment: ${{ matrix.environment }}

    steps:
      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ matrix.azure-client-id }}
          tenant-id: ${{ matrix.azure-tenant-id }}
          subscription-id: ${{ matrix.azure-subscription-id }}

      - name: Deploy to ${{ matrix.environment }}
        run: |
          echo "Deploying to ${{ matrix.environment }}"
          az account show

Integration with Azure Storage

name: Deploy Static Website
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: uhg-runner
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Upload to Storage
        run: |
          az storage blob upload-batch \
            --account-name ${{ secrets.STORAGE_ACCOUNT }} \
            --destination '$web' \
            --source ./dist \
            --overwrite

Integration with Azure Container Registry

name: Build and Push Container
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  build-push:
    runs-on: uhg-runner
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Login to ACR
        run: |
          az acr login --name ${{ secrets.ACR_NAME }}

      - name: Build and Push
        run: |
          docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/myapp:${{ github.sha }} .
          docker push ${{ secrets.ACR_NAME }}.azurecr.io/myapp:${{ github.sha }}

Integration with Optum Artifactory

name: Build, Scan, and Deploy to Azure
on:
  push:
    branches: [main]

permissions:
  actions: read
  contents: write
  pull-requests: write
  security-events: write
  checks: write
  id-token: write

jobs:
  build-and-deploy:
    runs-on: [uhg-runner]
    steps:
      - uses: actions/checkout@v4

      # Configure Artifactory
      - name: Configure Artifactory Connection
        id: artifactory-setup
        uses: uhg-pipelines/epl-jf/configure-saas-connection@latest
        with:
          jfrog-project-key: your-project-key
          npm-setup: true

      # Build and publish to Artifactory
      - name: Build and Scan
        uses: optum-eeps/epl-actions/node-build-scan@v1
        with:
          jfrog-project-key: your-project-key
          jfrog-build-name: ${{ steps.artifactory-setup.outputs.jfrog-build-name }}
          jfrog-build-number: ${{ steps.artifactory-setup.outputs.jfrog-build-number }}
          npm-publish: true

      # Authenticate to Azure
      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      # Deploy to Azure
      - name: Deploy to Azure App Service
        run: |
          az webapp deployment source config-zip \
            --resource-group ${{ secrets.RESOURCE_GROUP }} \
            --name ${{ secrets.APP_NAME }} \
            --src ./dist.zip

Security Best Practices

1. Least Privilege Access

Grant only the minimum permissions required:

# Instead of Contributor at subscription level
# Use specific roles at resource level
az role assignment create \
  --assignee $APP_ID \
  --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$STORAGE"

2. Restrict Repository Access

Use specific subject patterns in federated credentials:

# Specific branch only
subject = "repo:OptumInsight-Platform/my-repo:ref:refs/heads/main"

# Specific environment only
subject = "repo:OptumInsight-Platform/my-repo:environment:production"

# Specific tag pattern
subject = "repo:OptumInsight-Platform/my-repo:ref:refs/tags/v*"

3. Environment-Specific Credentials

Create separate app registrations for each environment:

# Development
az ad app create --display-name "github-actions-dev"

# Staging
az ad app create --display-name "github-actions-staging"

# Production
az ad app create --display-name "github-actions-prod"

4. Conditional Access Policies

Implement Azure AD Conditional Access for additional security:

# Require specific IP ranges
# Configure through Azure Portal > Azure AD > Security > Conditional Access
# Create policy requiring corporate network or VPN

5. Audit and Monitoring

Enable logging and monitoring:

# Enable diagnostic logging for App Registration
az monitor diagnostic-settings create \
  --resource $APP_ID \
  --name "github-actions-audit" \
  --logs '[{"category": "AuditLogs", "enabled": true}]' \
  --workspace $LOG_ANALYTICS_WORKSPACE_ID

Troubleshooting

Common Issues

1. "AADSTS700016: Application not found"

# Verify app registration exists
az ad app list --display-name "your-app-name"

# Check if service principal was created
az ad sp list --filter "appId eq 'YOUR_CLIENT_ID'"

# Recreate service principal if missing
az ad sp create --id $APP_ID

2. "AADSTS70021: No matching federated identity"

# Verify federated credential configuration
az ad app federated-credential list --id $APP_ID

# Check the subject claim matches your repository
# Subject format: repo:ORG/REPO:ref:refs/heads/BRANCH
# or: repo:ORG/REPO:environment:ENVIRONMENT

# Verify issuer is correct
# Should be: https://token.actions.githubusercontent.com

# Verify audience is correct
# Should be: api://AzureADTokenExchange

3. "Authorization failed"

# Check role assignments
az role assignment list --assignee $APP_ID --output table

# Verify subscription ID matches
az account show

# Check if role has propagated (can take a few minutes)
sleep 60
az role assignment list --assignee $APP_ID --output table

4. "Token validation failed"

  • Verify workflow has id-token: write permission
  • Check that audience is api://AzureADTokenExchange
  • Ensure repository name and branch match the subject claim exactly

Debugging Steps

Debug Federated Credential Match

- name: Debug OIDC Token Claims
  env:
    ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }}
    ACTIONS_ID_TOKEN_REQUEST_URL: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}
  run: |
    # Get the OIDC token
    OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r .value)

    # Decode JWT payload
    echo $OIDC_TOKEN | cut -d'.' -f2 | base64 -d | jq .

Verify Azure CLI Authentication

# Check current authentication
az account show

# List accessible subscriptions
az account list --output table

# Verify service principal access
az login --service-principal \
  -u $APP_ID \
  -p <certificate-or-secret> \
  --tenant $TENANT_ID

Test Role Permissions

# Try to list resources with the service principal
az resource list --output table

# Test specific permission
az storage account list --output table

Advanced Patterns

1. Cross-Subscription Access

# Assign role in different subscription
resource "azurerm_role_assignment" "cross_sub" {
  scope                = "/subscriptions/${var.target_subscription_id}"
  role_definition_name = "Reader"
  principal_id         = azuread_service_principal.github_actions.object_id
}

2. Conditional Access Based on PR Labels

- name: Deploy to production
  if: contains(github.event.pull_request.labels.*.name, 'deploy-prod')
  uses: azure/login@v1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID_PROD }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_PROD }}

3. Multi-Region Deployment

strategy:
  matrix:
    region: [eastus, westus2, northeurope]
steps:
  - name: Azure Login
    uses: azure/login@v1
    with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

  - name: Deploy to ${{ matrix.region }}
    run: |
      az deployment group create \
        --resource-group rg-${{ matrix.region }} \
        --template-file deploy.json \
        --parameters location=${{ matrix.region }}

4. Managed Identity Integration

- name: Azure Login with OIDC
  uses: azure/login@v1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Access Key Vault using Managed Identity
  run: |
    # Service principal can access Key Vault
    az keyvault secret show \
      --vault-name ${{ secrets.KEY_VAULT_NAME }} \
      --name my-secret

Getting Latest Versions

GitHub CLI Commands

# Get latest azure/login action version
gh api repos/azure/login/releases/latest --jq '.tag_name'

# Check Azure CLI version
az version

# Update Azure CLI
# macOS
brew upgrade azure-cli

# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Azure CLI Commands

# List all app registrations
az ad app list --output table

# List all federated credentials for an app
az ad app federated-credential list --id $APP_ID --output table

# List role assignments
az role assignment list --assignee $APP_ID --output table

# Check role definition details
az role definition list --name "Contributor" --output json

Implementation Checklist

Pre-Setup

  • Azure subscription with appropriate permissions
  • Azure CLI installed and authenticated
  • Terraform installed (if using IaC approach)
  • GitHub repository in organization
  • Owner or Application Administrator role in Azure AD

Azure AD Configuration

  • App registration created
  • Service principal created
  • Federated identity credentials configured
  • Appropriate audience set (api://AzureADTokenExchange)
  • Subject claims match repository/branch pattern
  • Issuer set to https://token.actions.githubusercontent.com

RBAC Configuration

  • Appropriate roles assigned to service principal
  • Scope correctly set (subscription, resource group, or resource)
  • Least privilege principle applied
  • Role assignments propagated (wait ~60 seconds)

GitHub Actions

  • Workflow includes id-token: write permission
  • Client ID configured as repository secret
  • Tenant ID configured as repository secret
  • Subscription ID configured as repository secret
  • Test workflow validates authentication
  • Error handling implemented

Security Validation

  • Least privilege principle applied
  • Repository access properly restricted
  • Environment-based credentials configured
  • Monitoring and alerting configured
  • Separate app registrations per environment

Documentation

  • Team training on OIDC and Azure AD concepts
  • Runbooks for troubleshooting
  • Security policies documented
  • Regular review schedule established

Support Resources


Remember: Federated Identity Credentials eliminate the need for long-lived secrets and certificates for Azure authentication, significantly improving security posture while maintaining automation capabilities. Always use the principle of least privilege and restrict access to specific repositories, branches, or environments.