Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/WorkloadIdentityFederation/Get-AzWorkloadIdentityFederationToken.ps1

<#
.SYNOPSIS
    Acquires an Azure DevOps Bearer token via Workload Identity Federation.
 
.DESCRIPTION
    Exchanges a federated OIDC token (from Kubernetes/AKS, GitHub Actions, or a manually-supplied
    token) for an Azure AD Bearer token scoped to the Azure DevOps resource
    (499b84ac-1321-427f-aa17-267ca6975798), using the OAuth 2.0 JWT-bearer client assertion flow.
    Unlike certificate-based auth, no private key is ever held locally - the federated token
    itself is the assertion, pre-signed by the external identity provider.
 
.PARAMETER OrganizationName
    Azure DevOps organization name, used when verifying the token.
 
.PARAMETER TenantId
    Azure AD tenant ID.
 
.PARAMETER ClientId
    Application (client) ID of the service principal configured with a federated credential.
 
.PARAMETER FederatedTokenFile
    Path to a file containing the raw federated JWT (AKS/Kubernetes workload identity pattern).
 
.PARAMETER GitHubActions
    Acquire the federated token from the GitHub Actions OIDC endpoint.
 
.PARAMETER GitHubActionsAudience
    The audience requested from the GitHub Actions OIDC endpoint. Defaults to 'api://AzureADTokenExchange'.
 
.PARAMETER FederatedToken
    A federated JWT already obtained by the caller (e.g. via an Azure DevOps Pipelines OIDC task).
 
.PARAMETER Verify
    If set, verifies the token by calling the Azure DevOps API after acquisition.
#>

Function Get-AzWorkloadIdentityFederationToken
{
    [CmdletBinding(DefaultParameterSetName = 'File')]
    param (
        [Parameter(Mandatory = $true)]
        [String]$OrganizationName,

        [Parameter(Mandatory = $true)]
        [String]$TenantId,

        [Parameter(Mandatory = $true)]
        [String]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [String]$FederatedTokenFile,

        [Parameter(Mandatory = $true, ParameterSetName = 'GitHubActions')]
        [Switch]$GitHubActions,

        [Parameter(ParameterSetName = 'GitHubActions')]
        [String]$GitHubActionsAudience = 'api://AzureADTokenExchange',

        [Parameter(Mandatory = $true, ParameterSetName = 'Manual')]
        [String]$FederatedToken,

        [Parameter()]
        [Switch]$Verify
    )

    Write-Verbose "[Get-AzWorkloadIdentityFederationToken] Acquiring a workload identity federation token for tenant '$TenantId', client '$ClientId'."

    $federatedTokenSource = $PSCmdlet.ParameterSetName

    $assertion = $(
        switch ($federatedTokenSource)
        {
            'File'          { Get-AzFederatedAssertion -FederatedTokenFile $FederatedTokenFile }
            'GitHubActions' { Get-AzFederatedAssertion -GitHubActions -GitHubActionsAudience $GitHubActionsAudience }
            'Manual'        { Get-AzFederatedAssertion -FederatedToken $FederatedToken }
        }
    )

    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/token"
    $body = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" +
            "&client_id=$([Uri]::EscapeDataString($ClientId))" +
            "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" +
            "&client_assertion=$([Uri]::EscapeDataString($assertion))" +
            "&resource=499b84ac-1321-427f-aa17-267ca6975798"

    try
    {
        $response = Invoke-RestMethod -Uri $tokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded'
    }
    catch
    {
        throw "[Get-AzWorkloadIdentityFederationToken] Failed to acquire token from '$tokenEndpoint'. Error: $_"
    }

    if ([String]::IsNullOrEmpty($response.access_token))
    {
        throw "[Get-AzWorkloadIdentityFederationToken] Access token not returned. Verify TenantId, ClientId, and the federated credential configuration on the app registration."
    }

    Write-Verbose "[Get-AzWorkloadIdentityFederationToken] Token acquired successfully."

    # Only the 'File' source is meaningfully re-readable on refresh; store the path so
    # Update-AzWorkloadIdentityFederation can re-read it (the platform rotates its contents).
    $storedFederatedTokenFile = if ($federatedTokenSource -eq 'File') { $FederatedTokenFile } else { '' }

    $token = New-WorkloadIdentityFederationToken -TokenObj $response -TenantId $TenantId -ClientId $ClientId -FederatedTokenSource $federatedTokenSource -FederatedTokenFile $storedFederatedTokenFile

    if (-not $Verify)
    {
        return $token
    }

    Write-Verbose "[Get-AzWorkloadIdentityFederationToken] Verifying the connection to the Azure DevOps API."

    if (-not (Test-AzToken $token))
    {
        throw "[Get-AzWorkloadIdentityFederationToken] Token verification failed. Unable to connect to Azure DevOps organization '$OrganizationName'."
    }

    Write-Verbose "[Get-AzWorkloadIdentityFederationToken] Token verified successfully."
    return $token
}