Functions/Public/Authentication.ps1

# Authentication, connection, and token management functions

Function Connect-NectarCloud {
    <#
        .SYNOPSIS
        Connects to Nectar DXP cloud and store the credentials for later use.
  
        .DESCRIPTION
        Connects to Nectar DXP cloud and store the credentials for later use.
          
        .PARAMETER CloudFQDN
        The FQDN of the Nectar DXP cloud.
  
        .PARAMETER TenantName
        The name of a Nectar DXP cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments
          
        .PARAMETER Credential
        The credentials used to access the Nectar DXP UI. Normally in username@domain.com format
          
        .PARAMETER CredSecret
        Use stored credentials saved via Set-Secret. Requires prior installation of Microsoft.PowerShell.SecretManagement PS module and an appropriate
        secret vault, such as Microsoft.PowerShell.SecretStore. Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on
        the local machine. This is the minimum requirement for using this feature.
        Install the modules by running:
        Install-Module Microsoft.PowerShell.SecretManagement
        Install-Module Microsoft.PowerShell.SecretStore
 
        Register a credential secret by doing something like: Set-Secret -Name NectarCreds -Vault SecretStore -Secret (Get-Credential)
          
        .PARAMETER EnvFromFile
        Use a CSV file called N10EnvList.csv located in the user's default Documents folder to show a list of environments to select from.
        Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if N10EnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default credentials to use for the selected environment. This feature uses the Microsoft.PowerShell.SecretManagement PS module,
        which must be installed and configured with a secret store prior to using this option.
        N10EnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, Secret".
        Each environment and Secret (if used) should be on their own separate lines
 
        .PARAMETER UseToken
        Use a JWT (JSON web token) to connect to Nectar DXP instead of using credentials. This feature uses the Microsoft.PowerShell.SecretManagement PS module,
        which must be installed and configured with a secret store prior to using this option.
        The PS SecretManagement module can use any number of 3rd party secret stores that provide access to centralized secret management tools, such as Keeper and AWS Secrets.
        Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on the local machine. This is the minimum requirement for using this feature.
        Install the modules by running:
        Install-Module Microsoft.PowerShell.SecretManagement
        Install-Module Microsoft.PowerShell.SecretStore
 
        When -UseToken is selected, the function will check for a secret called <envname>-accesstoken (ie contoso.nectar.services-accesstoken).
        The secret must contain two fields called AccessToken and RefreshToken and must be writable.
        The token itself can be generated in the Nectar DXP UI or via New-NectarToken (when logged in with a local account).
        The New-NectarTokenRegistration can be used to generate a token using the default secret store (if supported by the secret store).
        If using the default Microsoft SecretStore, you can generate a token and save it as a secret on the local machine by running:
        New-NectarToken -TokenName <tokenname> | New-NectarTokenRegistration -CloudFQDN <NectarDXPFQDN>
        ie. New-NectarToken -TokenName laptop | New-NectarTokenRegistration -CloudFQDN contoso.nectar.services
 
        .PARAMETER TokenIdentifier
        An optional unique identifier (such as script name or username) to use when retreiving secrets from a secret store shared by multiple parties/scripts
          
        .EXAMPLE
        $Cred = Get-Credential
        Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command
          
        .EXAMPLE
        Connect-NectarCloud -CloudFQDN contoso.nectar.services -CredSecret MyNectarCreds
        Connects to contoso.nectar.services Nectar DXP cloud using previously stored secret called MyNectarCreds
 
        .EXAMPLE
        Connect-NectarCloud -CloudFQDN contoso.nectar.services -UseToken
        Connects to contoso.nectar.services Nectar DXP cloud using previously stored token stored in a Microsoft Secret Vault called contoso.nectar.services-accesstoken
 
        .NOTES
        Version 2.0
    #>

    
    [Alias("cnc")]
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar DXP cloud name must be in FQDN format."
            }
        })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential]$Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CredSecret,
        [Parameter(Mandatory=$False)]
        [switch]$UseToken,
        [Parameter(Mandatory=$False)]
        [string]$TokenIdentifier
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\N10EnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $CloudFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "CloudFQDN: $CloudFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN)
                
                # Check for default tenant in N10EnvList.csv and use if available, but don't override if user explicitly set the TenantName
                If (!$PsBoundParameters['TenantName']) {
                    $TenantName = $EnvSet[$EnvPos].DefaultTenant
                    Write-Verbose "DefaultTenant: $TenantName"
                }
                
                # Check for secret in N10EnvList.csv and use if available
                $CredSecret = $EnvSet[$EnvPos].CredSecret
                Write-Verbose "Secret: $CredSecret"
            }
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
        If ((-not $Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = Read-Host "Enter the Nectar DXP cloud FQDN"
            $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)"
            $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
            $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        }
        ElseIf (($Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NectarCloud
        }
        
        # Ask for credentials if global Nectar creds aren't available
        If (((-not $Global:NectarCred) -And (-not $Credential)) -Or (($Global:NectarCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $CredSecret) -And (-Not $UseToken) -And (-Not $Global:NectarSecretName)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:NectarCred -And (-not $Credential)) {
            $Credential = $Global:NectarCred
        }

        # Set the token name based on the inputs
        If ($TokenIdentifier) {
            $NectarSecretName = "$($CloudFQDN)-$($TokenIdentifier)-accesstoken"
        }
        ElseIf ($UseToken) {
            $NectarSecretName = "$($CloudFQDN)-accesstoken"
        }

        # Check secret store for stored token
        If ($Global:NectarSecretName -ne $NectarSecretName -And $UseToken) {
            Write-Verbose "Attempting to retrieve $NectarSecretName from secret store"
            Try {
                $Global:NectarDefaultVault = (Get-SecretVault | Where-Object {$_.IsDefault -eq $True}).Name
                $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction SilentlyContinue #Stop

                # If no secret name was found, try to escape the dots with a \. Explicitly required for Keeper vaults, but may be used by others
                If (-Not $Global:NectarToken) {
                    $NectarSecretName = $NectarSecretName.Replace('.','\.')
                    $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction Stop
                }
                $Global:NectarSecretName = $NectarSecretName
            }
            Catch {
                Throw "Could not find access token for $CloudFQDN. Run New-NectarTokenRegistration to create one."
                Return
            }
        }
        
        # Pull credentials from secret if specified
        If ($CredSecret) {
            Try {
                $Credential = Get-Secret $CredSecret
            }
            Catch {
                Throw "Cannot find secret: $CredSecret"
            }
        }
        
        # Only run on first execution of Connect-NectarCloud or if the CloudFQDN has changed
        If ((-Not $Global:NectarCred -And -Not $Global:NectarSecretName) -Or (-Not $Global:NectarCloud) -Or ($Global:NectarCloud -ne $CloudFQDN)) {
            # Check and notify if updated Nectar PS module available
            [string]$InstalledNectarPSVer = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version
            
            If ($InstalledNectarPSVer -gt 0) {
                [string]$LatestNectarPSVer = (Find-Module Nectar10).Version
                If ($LatestNectarPSVer -gt $InstalledNectarPSVer) {
                    Write-Host "=============== Nectar PowerShell module version $LatestNectarPSVer available ===============" -ForegroundColor Yellow
                    Write-Host "You are running version $InstalledNectarPSVer. Type " -ForegroundColor Yellow -NoNewLine
                    Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine
                    Write-Host ' to update. NOTE: Close and reopen the PowerShell window for any module update to take effect.' -ForegroundColor Yellow
                }
            }
            
            # Create authorization header
            If ($UseToken) {  # Refresh token and create auth header
                Try {
                    $RefreshHeaders = @{
                        'x-refresh-token'    = $Global:NectarToken.RefreshToken
                        'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
                        'x-domain-name'        = $Global:NectarToken.DomainName
                    }
                    Write-Verbose "x-refresh-token = $($Global:NectarToken.RefreshToken)"
                    Write-Verbose "authorization = Bearer $($Global:NectarToken.AccessToken)"
                    Write-Verbose "x-domain-name = $($Global:NectarToken.DomainName)"
                    Write-Verbose "Renewing token via https://$CloudFQDN/aapi/jwt/token/renew"
                    $WebRequest = Invoke-WebRequest -Uri "https://$CloudFQDN/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing
                    Write-Verbose $WebRequest.Content
                    $WebResponse = ($WebRequest.Content | ConvertFrom-JSON)
                    $Global:NectarToken.AccessToken = $WebResponse.AccessToken
                    $Global:NectarToken.RefreshToken = $WebResponse.RefreshToken
                    $Global:NectarTokenRefreshTime = Get-Date

                    If ($Global:NectarDefaultVault -eq 'Keeper') {
                        Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken
                        Set-Secret -Name "$($Global:NectarSecretName).RefreshToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.RefreshToken
                    } Else {
                        Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken
                    }

                    $Headers = @{ 
                        'Authorization' = "Bearer $($Global:NectarToken.AccessToken)"
                        'x-domain-name' = $Global:NectarToken.DomainName
                    }            

                    $ConnectionString = $NectarSecretName.Replace('\.','.')
                    # Remove credential global variables
                    Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue
                } Catch {
                    Throw $_
                }                
            } Else { # Create basic auth header
                $UserName = $Credential.UserName
                $Password = $Credential.GetNetworkCredential().Password
                $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($UserName):$($Password)"))
                $Headers = @{Authorization = "Basic $Base64AuthInfo"}
                $ConnectionString = $Credential.UserName
            }

            # Attempt connection to tenant
            $URI = "https://$CloudFQDN/dapi/info/network/types"
            Write-Verbose $URI
            $WebRequest = Invoke-WebRequest -Uri $URI -Method GET -Headers $Headers -UseBasicParsing -SessionVariable NectarSession

            If ($WebRequest.StatusCode -ne 200) {
                Throw "Could not connect to $CloudFQDN using $ConnectionString"
            }
            Else {
                Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow $ConnectionString
                $Global:NectarCloud         = $CloudFQDN
                $Global:NectarCred             = $Credential
                $Global:NectarAuthHeader    = $Headers
                $Global:NectarTimeZone        = (Get-NectarUserAccountSettings).Preferences.timeZone

                $UserInfo = Invoke-RestMethod -Uri "https://$Global:NectarCloud/adminapi/user" -Method GET -Headers $Global:NectarAuthHeader -WebSession $NectarSession

                # Extract any available service provider names
                $SPNames = $UserInfo.serviceProviderClients 
                If ($SPNames) { $SPNames = $SPNames | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name }

                # Create tenant list, combining tenant and service provider names. The double @() is because things get weird if there is only one tenant
                $Global:NectarTenantList = @(@($UserInfo.tenants.tenant) + $SPNames) | Sort-Object
                
                # If there is only one available tenant, assign that to the NectarTenantName global variable
                If ($Global:NectarTenantList.Count -eq 1) { $Global:NectarTenantName = $Global:NectarTenantList }
            }
        }

        # If token was used, check the last refresh time and update it if its more than 90 minutes old
        If ($Global:NectarToken -And (New-TimeSpan -Start $Global:NectarTokenRefreshTime -End (Get-Date)).TotalMinutes -gt 90) {
            $RefreshHeaders = @{
                'x-refresh-token'    = $Global:NectarToken.RefreshToken
                'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
            }

            If ($Global:NectarToken.DomainName -ne '') {
                $RefreshHeaders.Add('x-domain-name', $Global:NectarToken.DomainName)
            }
            
            Write-Verbose 'Refreshing token'
            $WebRequest = Invoke-WebRequest -Uri "https://$($Global:NectarCloud)/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing -SessionVariable NectarSession
            $Global:NectarToken.AccessToken = ($WebRequest.Content | ConvertFrom-JSON).AccessToken
            $Global:NectarTokenRefreshTime = Get-Date

            If ($Global:NectarDefaultVault -eq 'Keeper') {
                Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken
            }
            Else {
                Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken
            }

            $Headers = @{
                'authorization'    = "Bearer $($Global:NectarToken.AccessToken)"
            }

            $Global:NectarAuthHeader = $Headers
        }

        # Check to see if tenant name was entered and set global variable, if valid.
        If ($TenantName) {
            Try {
                If ($Global:NectarTenantList -Contains $TenantName) {
                    $Global:NectarTenantName = $TenantName.ToLower()
                    
                    # Get the large tenant mode for the tenant
                    $Global:NectarLargeTenantMode = [bool]([int]::Parse((Get-NectarTenantSettings -Parameter large_tenant -TenantName $TenantName).value))

                    Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine
                    Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine
                    Write-Host -ForegroundColor Green ". This tenant name will be used in all subsequent commands."
                }
                Else {
                    $TList = $Global:NectarTenantList -join ', '
                    Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            Catch {
                # Just set the tenant name if we are unable to validate the tenant name.
                $Global:NectarTenantName = $TenantName
                Write-Host -ForegroundColor Green "Set the tenant name to " -NoNewLine
                Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine
                Write-Host -ForegroundColor Green " but unable to verify if the tenant exists. This tenantname will be used in all subsequent commands."
            }
        }
        ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NectarTenantName global variable only if TenantName is explicitly set to NULL
            Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
        }
    }
}


Function Disconnect-NectarCloud {
    <#
        .SYNOPSIS
        Disconnects from any active Nectar DXP connection
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables
 
        .EXAMPLE
        Disconnect-NectarCloud
        Disconnects from all active connections to Nectar DXP tenants
 
        .NOTES
        Version 1.1
    #>

    [Alias("dnc")]
    [cmdletbinding()]
    param ()
    
    Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarCloud -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTenantList -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarAuthHeader -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarSecretName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarToken -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTokenRefreshTime -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarDefaultVault -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTimeZone -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarLargeTenantMode -Scope Global -ErrorAction:SilentlyContinue
}


Function Get-NectarToken {
    <#
        .SYNOPSIS
        Returns a list of all JWT tokens assigned to the logged in user
         
        .DESCRIPTION
        Returns a list of all JWT tokens assigned to the logged in user
         
        .EXAMPLE
        Get-NectarToken
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param ()

    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
        }
    }
}


Function New-NectarToken {
    <#
        .SYNOPSIS
        Creates a new JSON web token (JWT) to be used in scripts
         
        .DESCRIPTION
        Creates a new JSON web token (JWT) to be used in scripts
 
        .PARAMETER TokenName
        A descriptive name for the token
 
        .EXAMPLE
        New-NectarToken -TokenName 'ScriptToken'
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [string]$TokenName
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }
        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token?tokenName=$TokenName&validityDays=$ValidityDuration&tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
            Write-Error "Could not create access token. $($_.Exception.Message)"
        }
    }
}


Function Update-NectarToken {
    <#
        .SYNOPSIS
        Refreshes an expired Nectar token
         
        .DESCRIPTION
        Refreshes the currently used Nectar token
 
        .EXAMPLE
        Update-NectarToken
 
        .NOTES
        Version 1.0
    #>

    $RefreshHeaders = @{
        'x-refresh-token'    = $Global:NectarToken.RefreshToken
        'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
        'x-domain-name'        = $Global:NectarToken.DomainName
    }

    Try {
        $Global:NectarToken.AccessToken = Invoke-RestMethod -Uri "https://$Global:NectarCloud/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders
        
        $Headers = @{
            'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
        }
        
        $Global:NectarAuthHeader = $Headers
        Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Secret $Global:NectarToken.AccessToken
        Write-Host -ForegroundColor Green "Successfully updated " -NoNewLine
        Write-Host -ForegroundColor Yellow $Global:NectarSecretName
    }
    Catch {
        Throw "Error refreshing $($Global:NectarSecretName)"
    }
}


Function Remove-NectarToken {
    <#
        .SYNOPSIS
        Remove a Nectar token
         
        .DESCRIPTION
        Remove a Nectar token
 
        .PARAMETER RefreshToken
        The GUID of a refresh token to remove.
 
        .EXAMPLE
        Remove-NectarToken -AccessToken fd173c75-891c-4357-b5a3-0855c2a56299
 
        .EXAMPLE
        Get-NectarToken | Where-Object {$_.Name -eq 'Testing'} | Remove-NectarToken
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$RefreshToken
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        $RefreshHeaders =  $Global:NectarAuthHeader
        $RefreshHeaders['x-refresh-token'] = $RefreshToken

        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token/revoke"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method DELETE -URI $URI -Headers $RefreshHeaders
            Return "Total number of tokens removed: $JSON"
        }
        Catch {
            Write-Error "Could not delete access token with refresh token $($RefreshToken). $($_.Exception.Message)"
        }
    }
}


Function New-NectarTokenRegistration {
    <#
        .SYNOPSIS
        Registers a Nectar DXP token for connecting to Nectar DXP using JWT
 
        .DESCRIPTION
        Registers a Nectar DXP token for connecting to Nectar DXP using JWT. One-time task required before attempting to access Nectar DXP APIs.
        Stored using Microsoft SecretManagement PS module.
        If SecretManagement PS module is is not installed, install via:
        Install-Module Microsoft.PowerShell.SecretManagement
 
        There are several PS modules that connect to different secret providers. Install the one appropriate for your situation prior to running this command.
        For example, the PS SecretStore stores secrets on the local machine and can be installed via:
        Install-Module Microsoft.PowerShell.SecretStore
         
        .PARAMETER CloudFQDN
        The FQDN of the Nectar DXP cloud.
 
        .PARAMETER Identifier
        An optional unique identifier (such as script name or username) to use when saving secrets to a secret store shared by multiple parties/scripts
 
        .PARAMETER AccessToken
        The access token to use for connecting to Nectar DXP
 
        .PARAMETER RefreshToken
        The refresh token used to refresh the Nectar DXP access token every 2 hours
 
        .PARAMETER DomainName
        The name of the email domain to use for the token. Should be the same one used for the user's login
         
        .PARAMETER SecretVault
        The name of the secret vault to install the secret. Use if installing to non-default secret vault
 
        .EXAMPLE
        Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        # [ValidateScript ({
        # If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
        # $True
        # }
        # Else {
        # Throw "ERROR: Nectar DXP cloud name must be in FQDN format."
        # }
        # })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Identifier,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$AccessToken,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$RefreshToken,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DomainName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SecretVault
    )
    
    Begin {
        # Verify the SecretManagement module is installed
        If (!(Get-InstalledModule -Name 'Microsoft.PowerShell.SecretManagement' -ErrorAction SilentlyContinue)) {
            Throw "SecretManagement module not installed. Please install using 'Install-Module Microsoft.PowerShell.SecretManagement'"
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }

        If (($Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NectarCloud
        }

        # Make sure CloudFQDN does not have extraneous characters and just includes the FQDN
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
        $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        
        # Create hash table with token information
        $TokenData = @{
            'AccessToken'     = $AccessToken
            'RefreshToken'    = $RefreshToken
            'DomainName'    = $DomainName    
        }

        If ($Identifier) {
            $SecretName = "$($CloudFQDN)-$($Identifier)-accesstoken"
        }
        Else {
            $SecretName = "$($CloudFQDN)-accesstoken"
        }

        $Params = @{
            'Name'        = $SecretName
            'Secret'    = $TokenData
        }

        If ($SecretVault) {$Params.Add('Vault',$SecretVault)}

        Set-Secret @Params
        Write-Host "Successfully created $SecretName"
    }
}


Function Get-NectarConnectionInfo {
    <#
        .SYNOPSIS
        Shows information about the active Nectar DXP connection
         
        .DESCRIPTION
        Shows information about the active Nectar DXP connection
 
        .EXAMPLE
        Get-NectarConnectionInfo
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnci")]
    [cmdletbinding()]
    param ()
    
    $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential
    $CloudInfo.CloudFQDN = $Global:NectarCloud
    $CloudInfo.Credential = ($Global:NectarCred).UserName
    $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo'
    
    Try {
        $TenantCount = Get-NectarTenantNames
        If ($TenantCount.Count -gt 1) {
            If ($Global:NectarTenantName) {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NectarTenantName
            }
            Else {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>'
            }
        }
    }
    Catch {
    }
    
    Return $CloudInfo
}


Function Get-MSGraphAccessToken {
    <#
        .SYNOPSIS
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
         
        .DESCRIPTION
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .PARAMETER CertFriendlyName
        The friendly name of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertThumbprint
        The thumbprint of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertPath
        The path to a PFX certificate to be used for certificate authentication. Can be used instead of MSClientSecret
         
        .PARAMETER CertStore
        The certificate store to be used for certificate authentication. Select either LocalMachine or CurrentUser. Used in conjunction with CertThumbprint or CertFriendlyName
        Can be used instead of MSClientSecret.
 
        .PARAMETER Scope
        The scope for the associated access token. Select from GraphAPI or Teams. Defaults to GraphAPI
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Obtains an authtoken for the given tenant using secret-based auth and saves the results for use in other commands in a variable called $AuthToken
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 029834092-234234-234234-23442343 -MSTenantID 234234234-234234-234-23-42342342 -CertFriendlyName 'CertAuth' -CertStore LocalMachine
        Obtains an authtoken for the given tenant using certificate auth and saves the results for use in other commands in a variable called $AuthToken
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$N10Cert,
        [Parameter(Mandatory=$False)]
        [string]$CertFriendlyName,
        [Parameter(Mandatory=$False)]
        [string]$CertThumbprint,
        [Parameter(Mandatory=$False)]
        [string]$CertPath,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LocalMachine','CurrentUser', IgnoreCase=$True)]
        [string]$CertStore = 'CurrentUser',
        [Parameter(Mandatory=$False)]
        [ValidateSet('GraphAPI','TeamsPS','TeamsAPI','Test', IgnoreCase=$True)]
        [string]$Scope = 'GraphAPI',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Switch ($Scope) {
            'GraphAPI'    { $AppliedScope = 'https://graph.microsoft.com/.default' }
            'TeamsPS'    { $AppliedScope = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default' }  # Can be used to connect to TeamsPS module along with Graph API token: Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
            'TeamsAPI'    { $AppliedScope = 'https://ring0.api.interfaces.records.teams.microsoft.com/.default' }  # Requires Azure app with permission 'Skype and Teams Tenant Admin API/application_access'
            'Test'        { $AppliedScope = 'https://api.interfaces.records.teams.microsoft.com/user_impersonation/.default' } 
        }
    }
    Process {
        If ($MSClientSecret) {
            # Get the Azure Graph API auth token
            $AuthBody = @{
                grant_type         = 'client_credentials'
                client_id         = $MSClientID
                client_secret    = $MSClientSecret
                scope             = $AppliedScope
            }
            
            $URI = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token"
            Write-Verbose $URI
            $JSON_Auth = Invoke-RestMethod -Method POST -URI $URI -Body $AuthBody
            $AuthToken = $JSON_Auth.access_token
            
            Return $AuthToken
        }
        Else {
            <#
            Needs access to the full certificate stored in Nectar DXP, and can be exported to PEM via Get-NectarMSTeamsSubscription (only if you have global admin privs)
              Get-NectarMSTeamsSubscription -ExportCertificate
             
            Need to create a certificate from the resulting PEM files using the following command:
            openssl pkcs12 -export -in TeamsCert.pem -inkey TeamsPriv.key -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -out FullCert.pfx
            Requires that OpenSSL is installed
 
            To convert a PFX to a Kubernetes secret in Base64 format, run the following:
              $fileContentBytes = get-content FullCert.pfx -AsByteStream
              [System.Convert]::ToBase64String($fileContentBytes) | Out-File pfx-encoded-bytes.txt
 
            Then use the resulting certificate to obtain an access token:
            $GraphToken = Get-MSGraphAccessToken -MSTenantID <tenantID> -MSClientID <Client/AppID> -CertPath .\FullCert.pfx
             
            Then use the resulting token in other commands like:
            Test-MSTeamsConnectivity -AuthToken $GraphToken
            #>

                        
            # First try to get the certificate automatically from the user's certificate store
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
            $Certificate = Get-ChildItem Cert:\CurrentUser\My -ErrorAction SilentlyContinue | Where-Object {$_.FriendlyName -eq $TenantName}

            # Get the certificate information via one of several methods
            If ($CertThumbprint) { $Certificate = Get-Item Cert:\$CertStore\My\$CertThumbprint }
            If ($CertFriendlyName) { $Certificate = Get-ChildItem Cert:\$CertStore\My | Where-Object {$_.FriendlyName -eq $CertFriendlyName} }
            If ($CertPath) { $Certificate = Get-PfxCertificate -FilePath $CertPath }
            If ($N10Cert) { 
                # Get certificate BASE64 encoding from N10
                $CertBlob = (Get-NectarMSTeamsSubscription).msClientCertificateDto.certificate
                $CertRaw = $CertBlob -replace "-----BEGIN CERTIFICATE-----", $NULL -replace "-----END CERTIFICATE-----", $NULL
                $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $Certificate.Import([Convert]::FromBase64String($CertRaw))            
            }
            
            If ($Certificate) {
                # Adapted from https://adamtheautomator.com/microsoft-graph-api-powershell/
                # Create base64 hash of certificate
                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
                
                # Create JWT timestamp for expiration
                $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
                $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
                $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

                # Create JWT validity start timestamp
                $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
                $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

                # Create JWT header
                $JWTHeader = @{
                    alg = "RS256"
                    typ = "JWT"
                    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
                    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
                }

                # Create JWT payload
                $JWTPayload = @{
                    # What endpoint is allowed to use this JWT
                    aud = "https://login.microsoftonline.com/$MSTenantID/oauth2/token"

                    # Expiration timestamp
                    exp = $JWTExpiration

                    # Issuer = your application
                    iss = $MSClientID

                    # JWT ID: random guid
                    jti = [guid]::NewGuid()

                    # Not to be used before
                    nbf = $NotBefore

                    # JWT Subject
                    sub = $MSClientID
                }

                # Convert header and payload to base64
                $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
                $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

                $JWTPayloadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
                $EncodedPayload = [System.Convert]::ToBase64String($JWTPayloadToByte)

                # Join header and Payload with "." to create a valid (unsigned) JWT
                $JWT = $EncodedHeader + "." + $EncodedPayload

                # Get the private key object of your certificate
                $PrivateKey = $Certificate.PrivateKey
                
                # Define RSA signature and hashing algorithm
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 

                # Create a signature of the JWT
                ##### Breaks down here. Only works with full cert downloaded/installed #####
                $Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '='

                # Join the signature to the JWT with "."
                $JWT = $JWT + "." + $Signature
                
                # Create a hash with body parameters
                $Body = @{
                    client_id                 = $MSClientID
                    client_assertion         = $JWT
                    client_assertion_type     = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
                    scope                     = $AppliedScope
                    grant_type                 = 'client_credentials'
                }

                $Uri = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token"
                Write-Verbose $URI

                # Use the self-generated JWT as Authorization
                $Header = @{
                    Authorization = "Bearer $JWT"
                }

                # Splat the parameters for Invoke-Restmethod for cleaner code
                $PostSplat = @{
                    ContentType = 'application/x-www-form-urlencoded'
                    Method = 'POST'
                    Body = $Body
                    Uri = $Uri
                    Headers = $Header
                }

                $Request = Invoke-RestMethod @PostSplat
                
                $AuthToken = $Request.access_token
                Return $AuthToken
            }
            Else {
                Write-Error "Could not find certificate in $CertStore. $($_.Exception.Message)"
            }
        }
    }
}


Function Test-MSTeamsConnectivity {
    <#
        .SYNOPSIS
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
         
        .DESCRIPTION
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
                 
        .PARAMETER SkipUserCount
        Skips the user count
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-NectarMSTeamsSubscription -TenantName contoso | Test-MSAzureADAccess
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$HideOutput,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken        
    )

    Begin {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
    }
    Process {
        If ($MSTenantID) { 
            $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
        }
        ElseIf (!$AuthToken) {
            $AuthToken = Get-NectarMSTeamsConfig -TenantName $TenantName | Get-MSGraphAccessToken
        }
    
        If (!$AuthToken) { Throw "Could not obtain auth token for tenant $TenantName. Does the tenant exist and have a valid MSTeams config?" }

        $Headers = @{
            Authorization = "Bearer $AuthToken"
        }

        $AccessResults = [pscustomobject][ordered]@{
            [string]'TeamsCallRecords'    = 'FAIL'
            [string]'TeamsDevices'        = 'FAIL'
            [string]'AzureUsers'        = 'FAIL'
            [string]'AzureGroups'        = 'FAIL'
        }
        
        # Test MS Teams call record access
        Try {
            $FromDateTime = (Get-Date -Format 'yyyy-MM-ddT00:00:00Z')
            $URI = "https://graph.microsoft.com/v1.0/communications/callRecords?`$filter=startDateTime ge $FromDateTime"
            Write-Verbose $URI
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Teams CR Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.TeamsCallRecords = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        # Test MS Teams device access
        Try {
            $URI = 'https://graph.microsoft.com/beta/teamwork/devices'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Teams Device Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.TeamsDevices = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        # Test Azure AD user access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/users'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Azure AD User Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers
            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.AzureUsers = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
            Get-JSONErrorStream -JSONResponse $_
            $SkipUserCount = $True
        }

        # Test Azure AD group access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/groups'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Azure AD Group Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -URI $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.AzureGroups = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        If ($AccessResults.AzureUsers -eq 'PASS') { $UserCount = Get-MSTeamsUserLicense -AuthToken $AuthToken -TenantName $TenantName }
        
        Clear-Variable AuthToken

        $Results = @{
            AccessResults    = $AccessResults
            UserCount        = $UserCount
        }
        Return $Results
    }
}


Function Get-NectarDefaultTenantName {
    <#
        .SYNOPSIS
        Set the default tenant name for use with other commands.
         
        .DESCRIPTION
        Set the default tenant name for use with other commands.
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("stne")]
    Param ()

    If ($Global:NectarTenantName) { # Use globally set tenant name, if one was set and not explicitly included in the command
        Return $Global:NectarTenantName 
    }
    ElseIf (!$TenantName -And !$Global:NectarTenantName) { # If a tenant name wasn't set (normal for most connections, set the TenantName variable if only one available
        $URI = "https://$Global:NectarCloud/aapi/tenant"
        Write-Verbose $URI
        $TenantList = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader
        If ($TenantList.Count -eq 1) {
            Return $TenantList
        }
        Else {
            $TenantList | ForEach-Object { $TList += ($(If($TList){", "}) + $_) }
            Write-Error "TenantName was not specified. Select one of $TList"
            $PSCmdlet.ThrowTerminatingError()
            Return 
        }
    }
}