Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Authentication/Certificate/Get-AzServicePrincipalCertificateToken.ps1

<#
.SYNOPSIS
    Acquires an Azure DevOps Bearer token using a Service Principal certificate assertion.
 
.DESCRIPTION
    Posts a signed JWT assertion to the Azure AD token endpoint to obtain a Bearer token
    scoped to the Azure DevOps resource. Supports both Windows certificate store thumbprint
    and cross-platform PFX file loading.
 
.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.
 
.PARAMETER CertificateThumbprint
    SHA-1 thumbprint of the certificate in the current user or local machine cert store.
 
.PARAMETER CertificatePath
    Path to a .pfx certificate file. Used for cross-platform scenarios.
 
.PARAMETER CertificatePassword
    Password protecting the PFX file, as a SecureString.
 
.PARAMETER Verify
    If set, verifies the token by calling the Azure DevOps API.
#>

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

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

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

        [Parameter(Mandatory = $true, ParameterSetName = 'Thumbprint')]
        [String]$CertificateThumbprint,

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

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [SecureString]$CertificatePassword,

        [Parameter()]
        [Switch]$Verify
    )

    Write-Verbose "[Get-AzServicePrincipalCertificateToken] Acquiring certificate-based token for tenant '$TenantId', client '$ClientId'."

    # Load the certificate
    if ($PSCmdlet.ParameterSetName -eq 'Thumbprint')
    {
        Write-Verbose "[Get-AzServicePrincipalCertificateToken] Loading certificate from store with thumbprint '$CertificateThumbprint'."

        $cert = Get-Item "Cert:\CurrentUser\My\$CertificateThumbprint" -ErrorAction SilentlyContinue
        if ($null -eq $cert)
        {
            $cert = Get-Item "Cert:\LocalMachine\My\$CertificateThumbprint" -ErrorAction SilentlyContinue
        }
        if ($null -eq $cert)
        {
            throw "[Get-AzServicePrincipalCertificateToken] Certificate with thumbprint '$CertificateThumbprint' not found in CurrentUser\My or LocalMachine\My."
        }
    }
    else
    {
        Write-Verbose "[Get-AzServicePrincipalCertificateToken] Loading certificate from file '$CertificatePath'."

        if (-not (Test-Path -Path $CertificatePath))
        {
            throw "[Get-AzServicePrincipalCertificateToken] Certificate file not found at path '$CertificatePath'."
        }

        $BSTR           = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($CertificatePassword)
        $plainPassword  = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)

        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
            $CertificatePath,
            $plainPassword,
            [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
        )
    }

    # Build JWT assertion
    $jwtAssertion = Build-JWTAssertion -Certificate $cert -TenantId $TenantId -ClientId $ClientId

    $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($jwtAssertion))" +
            "&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-AzServicePrincipalCertificateToken] Failed to acquire token from '$tokenEndpoint'. Error: $_"
    }

    if ([String]::IsNullOrEmpty($response.access_token))
    {
        throw "[Get-AzServicePrincipalCertificateToken] Access token not returned. Verify TenantId, ClientId, and certificate configuration."
    }

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

    if ($PSCmdlet.ParameterSetName -eq 'Thumbprint')
    {
        $token = New-CertificateToken -TokenObj $response -TenantId $TenantId -ClientId $ClientId -Thumbprint $CertificateThumbprint
    }
    else
    {
        $token = New-CertificateTokenFromFile -TokenObj $response -TenantId $TenantId -ClientId $ClientId -CertPath $CertificatePath -CertPassword $CertificatePassword
    }

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

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

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

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