Private/Authorization.ps1

function Get-ClientCredentials {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Target
    )

    # Retrieve stored credentials from Windows Credential Manager
    try {
        $storedCredentials = Get-StoredCredential -Target $Target -ErrorAction Stop
    }
    catch {
        $message = "Failed to read stored credentials for target '$Target'. $($_.Exception.Message)"
        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }

    if (-not $storedCredentials -or
        [string]::IsNullOrWhiteSpace($storedCredentials.UserName) -or
        $null -eq $storedCredentials.Password) {

        $message = "Credentials not found or incomplete for target '$Target'."
        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }

    # Convert SecureString password to plain text
    $bstr = [IntPtr]::Zero
    try {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($storedCredentials.Password)
        $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($bstr)
    }
    finally { # Zero and free the unmanaged buffer
        if ($bstr -ne [IntPtr]::Zero) {
            [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
        }
    }

    [pscustomobject]@{
        ClientId     = $storedCredentials.UserName
        ClientSecret = $plainSecret
    }
}

function Get-Jwt {
    [CmdletBinding()]
    param ()

    $uri = "https://{0}-login.{1}.nexthink.cloud{2}" -f `
        $Config.NexthinkAPI.InstanceName, `
        $Config.NexthinkAPI.Region, `
        $MAIN.APIs.OAuth.Path

    Write-CustomLog -Message "Uri for JWT: $uri" -Severity 'DEBUG'
    Write-CustomLog -Message "Credential Target: $($Config.NexthinkAPI.OAuthCredentialEntry)" -Severity 'DEBUG'

    $oldProgress = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'

    try {
        $credentials = Get-ClientCredentials -Target $Config.NexthinkAPI.OAuthCredentialEntry

        $basicHeader = Get-StringAsBase64 -InputString ("{0}:{1}" -f $credentials.ClientId, $credentials.ClientSecret)

        $headers = $Config._API.headers
        $headers['Authorization'] = "Basic $basicHeader"

        $body = @{
            grant_type = 'client_credentials'
            scope      = 'service:integration'
        }

        $parameters = @{
            Uri             = $uri
            Method          = 'POST'
            Headers         = $headers
            Body            = $body
            ContentType     = 'application/x-www-form-urlencoded'
            UseBasicParsing = $true
            ErrorAction     = 'Stop'
        }

        $response = Invoke-WebRequest @parameters

        Remove-Variable credentials, basicHeader -Force -ErrorAction SilentlyContinue

        if ($response.StatusCode -ne 200) {
            $message = "Error Code: $($response.StatusCode). Unable to access $uri endpoint."
            Write-CustomLog -Message $message -Severity 'ERROR'
            throw $message
        }

        $parsedResponse = $response.Content | ConvertFrom-Json
        $tokenDate      = [DateTime]$response.Headers.Date
        $expiresUtc     = $tokenDate.AddSeconds($parsedResponse.expires_in)

        Write-CustomLog -Message ("Retrieved JWT. Expires at {0:u}" -f $expiresUtc) -Severity 'DEBUG'

        [pscustomobject]@{
            token   = $parsedResponse.access_token
            expires = $expiresUtc
        }
    }
    catch [System.Net.WebException] {
        $ex = $_.Exception
        $statusCode = $null

        if ($ex.Response -and $ex.Response -is [System.Net.HttpWebResponse]) {
            $statusCode = [int]$ex.Response.StatusCode
        }

        if ($statusCode -eq 407) {
            $message = @"
Proxy authentication required when calling $uri.
If your environment uses an authenticating proxy, set `"Proxy.UseDefaultCredentials`" to true in config.json
and re-run Initialize-NexthinkAPI so the module can send default credentials to the proxy.
"@
.Trim()
        }
        else {
            $message = "Unable to access $uri endpoint. Details: $($ex.Message)"
        }

        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }
    catch [System.IO.IOException] {
        $message = "Unable to access $uri endpoint due to an I/O error. Details: $($_.Exception.Message)"
        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }
    catch {
        $message = "An unexpected error occurred while requesting JWT. Details: $($_.Exception.Message)"
        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }
    finally {
        $ProgressPreference = $oldProgress
    }
}

function Set-Headers {
    [CmdletBinding()]
    param ()

    if (-not $script:Config -or -not $Config._API) {
        throw "Nexthink API is not initialized. Call 'Initialize-NexthinkAPI' before running any functions"
    }

    if (-not $Config._API.headers) {
        throw "Nexthink API headers not initialized. 'Initialize-NexthinkAPI' did not complete successfully."
    }

    $now = Get-Date

    # Refresh if missing or expiring within 1 minute
    if (-not $Config._API.expires -or $Config._API.expires.AddMinutes(1) -lt $now) {
        # Obtain new JWT
        Write-CustomLog -Message "JWT is missing or expiring soon. Obtaining new token." -Severity 'DEBUG'
        $localToken = Get-Jwt

        $Config._API.expires = $localToken.expires
        $Config._API.headers['Authorization'] = "Bearer $($localToken.token)"
    }
}

function Set-SecurityProtocol {
    [CmdletBinding()]
    param ()

    $TLS_12 = 3072
    $TLS_13 = 12288

    # Always enable TLS 1.2
    $protocols = [enum]::ToObject([Net.SecurityProtocolType], $TLS_12)

    # Try to add TLS 1.3 if the runtime supports it
    try {
        $protocols = $protocols -bor [enum]::ToObject([Net.SecurityProtocolType], $TLS_13)
    }
    catch {
        # TLS 1.3 not available on this runtime – ignore
    }

    [Net.ServicePointManager]::SecurityProtocol = $protocols

    # Proxy handling driven by config
    $proxyConfig = $Config.Proxy

    if ($proxyConfig -and $proxyConfig.UseSystemProxy) {
        $proxy = [System.Net.WebRequest]::GetSystemWebProxy()

        if ($proxy) {
            if ($proxyConfig.UseDefaultCredentials) {
                # Opt-in only; otherwise we don't send creds to the proxy
                $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
            }

            [System.Net.WebRequest]::DefaultWebProxy = $proxy
        }
    }
    # else: leave DefaultWebProxy as-is (no proxy or whatever the process already has)
}

function Get-StringAsBase64 ([string]$InputString) {
    [CmdletBinding()]
    $Bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
    $EncodedText = [Convert]::ToBase64String($Bytes)
    return $EncodedText
}