internal/functions/Invoke-ARM.ps1
# Enhanced Invoke-ARM for orchestrator use with full OIDC support function Invoke-ARM { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$restURI, [Parameter(Mandatory = $true)] [string]$method, [string]$body, [string]$subscriptionId ) try { # Enhanced OIDC-compatible ARM token acquisition with GitHub Actions prioritization $token = $null $tokenAcquisitionErrors = @() $authMethod = "Unknown" # Method 1: Environment Variables (PRIORITIZED - Works best in GitHub Actions OIDC) # This method is prioritized because it's the most reliable in CI/CD environments if (-not $token -and ($env:AZURE_ACCESS_TOKEN -or $env:ARM_ACCESS_TOKEN)) { try { $token = $env:AZURE_ACCESS_TOKEN -or $env:ARM_ACCESS_TOKEN $authMethod = "Environment Variable (AZURE_ACCESS_TOKEN)" Write-Verbose "ARM token acquired from environment variable - GitHub Actions OIDC compatible" } catch { $tokenAcquisitionErrors += "Environment variable: $($_.Exception.Message)" } } # Method 2: Azure CLI (HIGHLY RELIABLE - Works with azure/login@v2 OIDC) # Azure CLI tokens work consistently with GitHub Actions OIDC setup if (-not $token) { try { # Check if Azure CLI is available and authenticated $cliCheck = az account show --query id --output tsv 2>$null if ($cliCheck) { $cliToken = az account get-access-token --resource https://management.azure.com/ --query accessToken --output tsv 2>$null if ($cliToken -and $cliToken.Trim() -ne "") { $token = $cliToken.Trim() $authMethod = "Azure CLI (GitHub Actions OIDC Compatible)" Write-Verbose "ARM token acquired from Azure CLI - works reliably with azure/login@v2" } else { throw "Azure CLI returned empty token" } } else { throw "Azure CLI not authenticated" } } catch { $tokenAcquisitionErrors += "Azure CLI: $($_.Exception.Message)" } } # Method 3: Azure PowerShell Context (May fail in some GitHub Actions environments) # Keep this as fallback since it can be unreliable with OIDC in some configurations if (-not $token) { try { $azContext = Get-AzContext -ErrorAction Stop if ($azContext -and $azContext.Account) { # Use Get-AzAccessToken for ARM resource (recommended approach) $tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com/" -ErrorAction Stop $token = if ($tokenObj.Token -is [System.Security.SecureString]) { # PowerShell version-aware SecureString conversion if ($PSVersionTable.PSVersion.Major -ge 7) { # PowerShell 7.x: Use ConvertFrom-SecureString -AsPlainText (recommended) ConvertFrom-SecureString -SecureString $tokenObj.Token -AsPlainText } else { # PowerShell 5.1: Use Marshal approach with -Force to avoid prompts try { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token)) } catch { # Fallback: Convert to encrypted string then back (less secure but compatible) $encryptedToken = ConvertFrom-SecureString -SecureString $tokenObj.Token -Force $secureToken = ConvertTo-SecureString -String $encryptedToken -Force [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)) } } } else { $tokenObj.Token } $authMethod = "Azure PowerShell Context (Fallback)" Write-Verbose "ARM token acquired from Azure PowerShell context" } } catch { $tokenAcquisitionErrors += "Azure PowerShell Context: $($_.Exception.Message)" } } # Method 4: Direct Environment Variable (Extended check) if (-not $token -and $env:AZURE_ACCESS_TOKEN) { try { $token = $env:AZURE_ACCESS_TOKEN $authMethod = "Environment Variable (AZURE_ACCESS_TOKEN)" Write-Verbose "ARM token acquired from environment variable" } catch { $tokenAcquisitionErrors += "Environment variable: $($_.Exception.Message)" } } # Method 3: Service Principal Authentication (Fallback) if (-not $token -and $env:AZURE_CLIENT_ID -and $env:AZURE_TENANT_ID) { try { $tokenEndpoint = "https://login.microsoftonline.com/$($env:AZURE_TENANT_ID)/oauth2/v2.0/token" $body = @{ client_id = $env:AZURE_CLIENT_ID scope = "https://management.azure.com/.default" grant_type = "client_credentials" } if ($env:AZURE_CLIENT_SECRET) { $body.client_secret = $env:AZURE_CLIENT_SECRET Write-Verbose "Using service principal with client secret for ARM token" } elseif ($env:AZURE_CLIENT_ASSERTION) { $body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" $body.client_assertion = $env:AZURE_CLIENT_ASSERTION Write-Verbose "Using service principal with client assertion for ARM token" } else { throw "Service principal requires AZURE_CLIENT_SECRET or AZURE_CLIENT_ASSERTION" } $response = Invoke-RestMethod -Uri $tokenEndpoint -Method POST -Body $body -ContentType "application/x-www-form-urlencoded" $token = $response.access_token $authMethod = "Service Principal (Direct OAuth2)" Write-Verbose "ARM token acquired via service principal authentication" } catch { $tokenAcquisitionErrors += "Service Principal: $($_.Exception.Message)" } } if (-not $token) { $errorMessage = @" Failed to acquire ARM access token for Azure Resource Manager API calls. Authentication Methods Attempted: $($tokenAcquisitionErrors | ForEach-Object { " - $_" } | Out-String) Recommended GitHub Actions Setup (Official Microsoft Pattern): 1. Use the azure/login@v2 action with OIDC in your workflow: jobs: deploy: permissions: id-token: write steps: - uses: azure/login@v2 with: client-id: `${{ secrets.AZURE_CLIENT_ID }} tenant-id: `${{ secrets.AZURE_TENANT_ID }} subscription-id: `${{ secrets.AZURE_SUBSCRIPTION_ID }} enable-AzPSSession: true 2. Configure federated identity credentials in Azure: - Issuer: https://token.actions.githubusercontent.com - Subject: repo:owner/repo:environment:production (or appropriate pattern) - Audience: api://AzureADTokenExchange Alternative Authentication Methods: - Set AZURE_ACCESS_TOKEN environment variable with a valid ARM token - Use service principal with AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET - Use managed identity with Connect-AzAccount in your script Current Environment Variables: AZURE_CLIENT_ID: $($null -ne $env:AZURE_CLIENT_ID) AZURE_TENANT_ID: $($null -ne $env:AZURE_TENANT_ID) AZURE_ACCESS_TOKEN: $($null -ne $env:AZURE_ACCESS_TOKEN) AZURE_CLIENT_SECRET: $($null -ne $env:AZURE_CLIENT_SECRET) AZURE_CLIENT_ASSERTION: $($null -ne $env:AZURE_CLIENT_ASSERTION) Azure PowerShell Context: $(if (Get-AzContext -ErrorAction SilentlyContinue) { "Available" } else { "Not Available" }) For more information: https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect "@ throw $errorMessage } Write-Verbose "ARM authentication successful using: $authMethod" $headers = @{ 'Authorization' = "Bearer $token" 'Content-Type' = 'application/json' } $params = @{ Uri = $restURI Method = $method Headers = $headers } if ($body) { $params['Body'] = $body } Write-Verbose "Making ARM API call: $method $restURI" $response = Invoke-RestMethod @params return $response } catch { Write-Error "ARM API call failed: $($_.Exception.Message)" Write-Verbose "Failed URI: $restURI" Write-Verbose "Method: $method" Write-Verbose "Body: $body" throw } } |