Classes/AzureDevOpsConnection.ps1

# Connection class for Azure DevOps Rest API
#
# Path: src/PSRule.Rules.AzureDevOps/Functions/Connection.ps1
# This class contains methods to connect to Azure DevOps Rest API
# using a service principal, managed identity or personal access token (PAT).
# it provides an authentication header which is refreshed automatically when it expires.
# --------------------------------------------------
#

class AzureDevOpsConnection {
    [string]$Organization
    [string]$PAT
    [string]$ClientId
    [string]$ClientSecret
    [string]$TenantId
    [string]$TokenEndpoint
    [string]$Token
    [System.DateTime]$TokenExpires
    [string]$AuthType
    [string]$TokenType
    

    # Constructor for Service Principal
    AzureDevOpsConnection(
        [string]$Organization,
        [string]$ClientId,
        [string]$ClientSecret,
        [string]$TenantId,
        [string]$TokenType = 'FullAccess'

    )
    {
        $this.Organization = $Organization
        $this.ClientId = $ClientId
        $this.ClientSecret = $ClientSecret
        $this.TenantId = $TenantId
        $this.TokenEndpoint = "https://login.microsoftonline.com/$($this.TenantId)/oauth2/v2.0/token"
        $this.Token = $null
        $this.TokenExpires = [System.DateTime]::MinValue
        $this.TokenType = $TokenType

        # Get a token for the Azure DevOps REST API
        $this.GetServicePrincipalToken()
    }

    # Constructor for Managed Identity
    AzureDevOpsConnection(
        [string]$Organization,
        [string]$TokenType = 'FullAccess'
    )
    {
        $this.Organization = $Organization
        # Get the Managed Identity token endpoint for the Azure DevOps REST API
        if(-not $env:IDENTITY_ENDPOINT) {
            $env:IDENTITY_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"
        }
        if($env:ADO_MSI_CLIENT_ID) {
            $this.TokenEndpoint = "$($env:IDENTITY_ENDPOINT)?resource=499b84ac-1321-427f-aa17-267ca6975798&api-version=2019-08-01&client_id=$($env:ADO_MSI_CLIENT_ID)"
        } else {
            $this.TokenEndpoint = "$($env:IDENTITY_ENDPOINT)?resource=499b84ac-1321-427f-aa17-267ca6975798&api-version=2019-08-01"
        }
        $this.Token = $null
        $this.TokenExpires = [System.DateTime]::MinValue
        $this.TokenType = $TokenType

        # Get a token for the Azure DevOps REST API
        $this.GetManagedIdentityToken()
    }

    # Constructor for Personal Access Token (PAT)
    AzureDevOpsConnection(
        [string]$Organization,
        [string]$PAT,
        [string]$TokenType = 'FullAccess'
    )
    {
        $this.Organization = $Organization
        $this.PAT = $PAT
        $this.Token = $null
        $this.TokenExpires = [System.DateTime]::MaxValue
        $this.TokenType = $TokenType

        # Get a token for the Azure DevOps REST API
        $this.GetPATToken()
    }

    # Get a token for the Azure DevOps REST API using a service principal
    [void]GetServicePrincipalToken()
    {
        $body = @{
            grant_type    = "client_credentials"
            client_id     = $this.ClientId
            client_secret = $this.ClientSecret
            scope         = '499b84ac-1321-427f-aa17-267ca6975798/.default'
        }
        # URL encode the client secret and id
        $secret = [System.Web.HttpUtility]::UrlEncode($this.ClientSecret)
        $id = [System.Web.HttpUtility]::UrlEncode($this.ClientId)
        #$body = "client_id=$($id)&client_secret=$($secret)&scope=499b84ac-1321-427f-aa17-267ca6975798/.default&grant_type=client_credentials"
        $header = @{
            'Content-Type' = 'application/x-www-form-urlencoded'
        }
        # POST as form url encoded body using the token endpoint
        $response = Invoke-RestMethod -Uri $this.TokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -Headers $header
        $this.Token = "Bearer $($response.access_token)"
        $this.TokenExpires = [System.DateTime]::Now.AddSeconds($response.expires_in)
        $this.AuthType = 'ServicePrincipal'
    }

    # Get a token for the Azure DevOps REST API using a managed identity
    [void]GetManagedIdentityToken()
    {
        $header = @{}
        If($env:IDENTITY_HEADER) {
            $header = @{ 'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER" ; Metadata = 'true'}
        } else {
            $header = @{ Metadata = 'true' }
        }
        $response = Invoke-RestMethod -Uri $this.TokenEndpoint -Method Get -Headers $header
        $this.Token = "Bearer $($response.access_token)"
        # Get token expiration time from the expires_on property and convert it from unix to a DateTime object
        $this.TokenExpires = (Get-Date 01.01.1970).AddSeconds($response.expires_on)
        $this.AuthType = 'ManagedIdentity'
    }

    # Get a token for the Azure DevOps REST API using a personal access token (PAT)
    [void]GetPATToken()
    {
        # base64 encode the PAT
        $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((":$($this.PAT)")))
        $this.Token = 'Basic ' + $base64AuthInfo
        $this.AuthType = 'PAT'
    }

    # Get the the up to date authentication header for the Azure DevOps REST API
    [System.Collections.Hashtable]GetHeader()
    {
        # If the token is expired, get a new one
        if ($this.TokenExpires -lt [System.DateTime]::Now) {
            switch ($this.AuthType) {
                'ServicePrincipal' {
                    $this.GetServicePrincipalToken()
                }
                'ManagedIdentity' {
                    $this.GetManagedIdentityToken()
                }
                'PAT' {
                    # PAT tokens don't expire
                }
            }
        }
        $header = @{
            Authorization = $this.Token
        }
        return $header
    }
}