Public/Connect-HaloAPI.ps1

using module ..\Classes\HaloLookup.psm1
using module ..\Classes\Completers\HaloAuthScopesCompleter.psm1
using module ..\Classes\Validators\HaloAuthScopesValidator.psm1
#Requires -Version 7

function Connect-HaloAPI {
    <#
        .SYNOPSIS
            Creates a new connection to a Halo instance.
        .DESCRIPTION
            Creates a new connection to a Halo instance and stores this in a PowerShell Session.
        .EXAMPLE
            PS C:\> Connect-HaloAPI -URL "https://example.halopsa.com" -ClientId "c9534241-dde9-4d04-9d45-32b1fbff22ed" -ClientSecret "14c0c9af-2db1-48ab-b29c-51975df4afa2-739e4ef2-9aad-4fe9-b486-794feca48ea8" -Scopes "all" -Tenant "demo" -VaultName "MyVault" -SaveToKeyVault $true
            This logs into Halo using the Client Credentials authorisation flow and saves the secrets to the specified Azure Key Vault for future use.
    #>

    [CmdletBinding(
        DefaultParameterSetName = 'Client Credentials'
    )]
    [OutputType([System.Void])]
    Param (
        # The URL of the Halo instance to connect to.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $True
        )]
        [URI]$URL,
        # The Client ID for the application configured in Halo.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $True
        )]
        [String]$ClientID,
        # The Client Secret for the application configured in Halo.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $True
        )]
        [String]$ClientSecret,
        # The API scopes to request, if this isn't passed the scope is assumed to be "all". Pass a string or array of strings. Limited by the scopes granted to the application in Halo.
        [Parameter(
            ParameterSetName = 'Client Credentials'
        )]
        [String[]]$Scopes = 'all',
        # The tenant name required for hosted Halo instances.
        [Parameter(
            ParameterSetName = 'Client Credentials'
        )]
        [String]$Tenant,
        # Hashtable containing additional parameters to be sent with each request.
        [Hashtable]$AdditionalHeaders,
        # If $true, retrieve parameters from Azure Key Vault. If $false, use parameters passed to function.
        [Parameter(
            ParameterSetName = 'Client Credentials'
        )]
        [bool]$UseKeyVault = $False,
        # The name of the secret in the Azure Key Vault.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $False
        )]
        [String]$SecretName,
        # The name of the Azure Key Vault.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $False
        )]
        [String]$VaultName,
        # If $true, save parameters to Azure Key Vault. If $false or not specified, do not save parameters.
        [Parameter(
            ParameterSetName = 'Client Credentials'
        )]
        [bool]$SaveToKeyVault = $False,
        # The object ID of the Managed Identity or Service Principal.
        [Parameter(
            ParameterSetName = 'Client Credentials',
            Mandatory = $False
        )]
        [String]$Identity
    )
    if ($UseKeyVault) {
        # If the Identity parameter is specified, use it to connect.
        # Otherwise, fall back to interactive login.
        if ($Identity) {
            Connect-AzAccount -Identity
        } else {
            Connect-AzAccount
        }
        $URL = (Get-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_URL").SecretValueText
        $ClientID = (Get-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_ClientID").SecretValueText
        $ClientSecret = (Get-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_ClientSecret").SecretValueText
    } elseif ($SaveToKeyVault) {
        # Save the URL, ClientID, and ClientSecret to the Azure Key Vault.
        if ($Identity) {
            Connect-AzAccount -Identity
        } else {
            Connect-AzAccount
        }
        $URL_Secret = ConvertTo-SecureString -String $URL -AsPlainText -Force
        Set-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_URL" -SecretValue $URL_Secret

        $ClientID_Secret = ConvertTo-SecureString -String $ClientID -AsPlainText -Force
        Set-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_ClientID" -SecretValue $ClientID_Secret

        $ClientSecret_Secret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
        Set-AzKeyVaultSecret -VaultName $VaultName -Name "${SecretName}_ClientSecret" -SecretValue $ClientSecret_Secret
    }

    # Convert scopes to space separated string if it's an array.
    if ($Scopes -is [system.array]) {
        $AuthScopes = $Scopes -Join ' '
    } else {
        $AuthScopes = $Scopes
    }
    # Build the authentication and base URLs.
    $AuthInfoURIBuilder = [System.UriBuilder]::New($URL)
    Write-Verbose "Looking up auth endpoint using the 'api/authinfo' endpoint."
    $AuthInfoURIBuilder.Path = 'api/authinfo'
    $AuthInfoParams = @{
        Uri = $AuthInfoURIBuilder.ToString()
        Method = 'GET'
        Headers = $AdditionalHeaders
    }
    do {
        $AuthInfoRetries++
        try {
            $AuthInfoResponse = Invoke-WebRequest @AuthInfoParams
        } catch [Microsoft.PowerShell.Commands.HttpResponseException] {
            $AuthInfoResponse = $False
            if ($_.Exception.Response.StatusCode.value__ -eq 429) {
                Write-Warning 'The request was throttled, waiting for 5 seconds.'
                Start-Sleep -Seconds 5
                continue
            } else {
                throw $_
                break
            }
        } catch {
            New-HaloError -ErrorRecord $_ -HasResponse
        }
    } while ((-not $AuthInfoResponse) -and ($AuthRetries -lt 10))
    if ($AuthInfoRetries -gt 1) {
        New-HaloError -ModuleMessage ('Retried auth info request {0} times, request unsuccessful.' -f $Retries)
    }
    if ($AuthInfoResponse.content) {
        $AuthInfo = $AuthInfoResponse.content | ConvertFrom-Json
        Write-Debug "Auth info response: $AuthInfo"
        $AuthURIBuilder = [System.UriBuilder]::New($AuthInfo.auth_url)
        Write-Verbose "Auth info found, using the '$($AuthInfo.auth_url)' endpoint."
        if ($AuthURIBuilder.Path) {
            $AuthURIBuilder.Path = $AuthURIBuilder.Path.TrimEnd('/') + '/token'
        } else {
            $AuthURIBuilder.Path = 'token'
        }
        
        if ($Tenant) {
            $AuthURIBuilder.Query = "tenant=$($Tenant)"
        } elseif ($AuthInfo.tenant_id) {
            $AuthURIBuilder.Query = "tenant=$($AuthInfo.tenant_id)"
        }
    } else {
        $AuthURIBuilder = [System.UriBuilder]::New($URL)
        Write-Warning 'Could not retrieve authentication URL from Halo falling back to default.'
        if ($Tenant) {
            $AuthURIBuilder.Path = 'auth/token'
            $AuthURIBuilder.Query = "tenant=$($Tenant)"
        } else {
            $AuthURIBuilder.Path = 'auth/token'
        }
    }
    $AuthenticationURI = $AuthURIBuilder.ToString()
    Write-Verbose "Using authentication URL: $($AuthenticationURI)"
    # Make sure URL is a base URI.
    $BaseURIBuilder = [System.UriBuilder]::New($URL)
    if ($BaseURIBuilder.Path) {
        $BaseURIBuilder.Path = $null
        $BaseURIBuilder.Query = $null
        $BaseURI = $BaseURIBuilder.ToString()
    }
    # Build a script-scoped variable to hold the connection information.
    $ConnectionInformation = @{
        URL = $BaseURI
        ClientID = $ClientID
        ClientSecret = $ClientSecret
        AuthScopes = $AuthScopes
        Tenant = $Tenant
        AdditionalHeaders = $AdditionalHeaders
    }
    Set-Variable -Name 'HAPIConnectionInformation' -Value $ConnectionInformation -Visibility Private -Scope Script -Force
    Write-Debug "Connection information set to: $($Script:HAPIConnectionInformation | Out-String)"
    # Halo authorisation request body.
    $AuthReqBody = @{
        grant_type = 'client_credentials'
        client_id = $Script:HAPIConnectionInformation.ClientID
        client_secret = $Script:HAPIConnectionInformation.ClientSecret
        scope = $Script:HAPIConnectionInformation.AuthScopes
    }
    # Build the WebRequest parameters.
    $WebRequestParams = @{
        Uri = $AuthenticationURI
        Method = 'POST'
        Body = $AuthReqBody
        ContentType = 'application/x-www-form-urlencoded'
        Headers = $AdditionalHeaders
    }
    do {
        $AuthRetries++
        try {
            $AuthReponse = Invoke-WebRequest @WebRequestParams
            $TokenPayload = ConvertFrom-Json -InputObject $AuthReponse.Content
            Write-Debug "Raw Token Payload: $($TokenPayload | Out-String)"
            # Build a script-scoped variable to hold the authentication information.
            $AuthToken = @{
                Type = $TokenPayload.token_type
                Access = $TokenPayload.access_token
                Expires = Get-TokenExpiry -ExpiresIn $TokenPayload.expires_in
                Refresh = $TokenPayload.refresh_token
                Id = $TokenPayload.id_token
            }
            Set-Variable -Name 'HAPIAuthToken' -Value $AuthToken -Visibility Private -Scope Script -Force
            Write-Verbose 'Got authentication token.'
            Write-Debug "Authentication token set to: $($Script:HAPIAuthToken | Out-String -Width 2048)"
            Write-Debug 'Initialising the Halo Lookup class cache.'
            $LookupTypes = Get-HaloLookup -LookupID 11
            if ($LookupTypes) {
                [HaloLookup]::LookupTypes = $LookupTypes
            } else {
                New-HaloError -ModuleMessage 'Could not retrieve lookup types from Halo.'
            }
            Write-Success "Connected to the Halo API with tenant URL $($Script:HAPIConnectionInformation.URL)"
            $Authenticated = $True
        } catch [Microsoft.PowerShell.Commands.HttpResponseException] {
            $Authenticated = $False
            if ($_.Exception.Response.StatusCode.value__ -eq 429) {
                Write-Warning 'The request was throttled, waiting for 5 seconds.'
                Start-Sleep -Seconds 5
                continue
            } else {
                throw $_
                break
            }
        } catch {
            New-HaloError -ErrorRecord $_
        }
    } while ((-not $Authenticated) -and ($AuthRetries -lt 10))
    if ($AuthRetries -gt 1) {
        New-HaloError -ModuleMessage ('Retried auth request {0} times, request unsuccessful.' -f $Retries)
    }
}