Public/New-ServiceNowSession.ps1

<#
.SYNOPSIS
Create a new ServiceNow session

.DESCRIPTION
Create a new ServiceNow session via credentials, OAuth, or access token.
This session will be used by default for all future calls.
Optionally, you can specify the api version you'd like to use; the default is the latest.

To use OAuth, ensure you've set it up, https://docs.servicenow.com/bundle/quebec-platform-administration/page/administer/security/task/t_SettingUpOAuth.html.

If using OAuth, the client credential will be stored in the script scoped variable ServiceNowSession and the access token will be automatically refreshed.

.PARAMETER Url
Base domain for your ServiceNow instance, eg. tenant.domain.com

.PARAMETER Credential
Username and password to connect. This can be used standalone to use basic authentication or in conjunction with ClientCredential for OAuth password grant.
Not required when using ClientCredential alone for the OAuth client_credentials grant (machine-to-machine).

.PARAMETER ClientCredential
Required for OAuth. Credential where the username is the Client ID and the password is the Secret.
If provided along with Credential, the OAuth password grant is used. If provided alone, the OAuth client_credentials grant is used, which does not require user credentials and is useful for machine-to-machine authentication or when MFA is enforced for interactive users.

.PARAMETER AccessToken
Provide the access token directly if obtained outside of this module.

.PARAMETER Proxy
Use a proxy server for the request, rather than connecting directly. Provide the full url.

.PARAMETER ProxyCredential
Credential of user who can access Proxy. If not provided, the current user will be used.

.PARAMETER ApiVersion
Specific API version to use. The default is the latest.

.PARAMETER GraphQL
Use GraphQL instead of REST calls

.PARAMETER GetAllTable
Populate $ServiceNowTable with data from all tables the user has access to

.PARAMETER TimeoutSec
Timeout in seconds for all operations.
The default is 0 which represents no timeout.

.PARAMETER RetryCount
Number of times to retry a request when a retryable HTTP error is received (429, 502, 503, 504, 408, 409).
The default is 2.

.PARAMETER RetryWaitSeconds
Number of seconds to wait between retries when no Retry-After header is provided.
The default is 5.

.PARAMETER MaxRetryAfterSeconds
Maximum number of seconds to honor from a Retry-After header.
If the server requests a wait longer than this value, the error will be thrown immediately.
The default is 10.

.PARAMETER PassThru
Provide the resulting session object to the pipeline as opposed to setting as a script scoped variable to be used by default for other calls.
This is useful if you want to have multiple sessions with different api versions, credentials, etc.

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -Credential $mycred
Create a new session using basic authentication and save it as the default.

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -Credential $mycred -GraphQL

Create a new session using basic authentication and save it as the default.
Use GraphQL instead of REST.

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -Credential $mycred -ClientCredential $myClientCred
Create a session using OAuth password grant and save it as the default

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -ClientCredential $myClientCred
Create a session using the OAuth client_credentials grant (machine-to-machine, no user context) and save it as the default

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -AccessToken 'asdfasd9f87adsfkksk3nsnd87g6s'
Create a session with an existing access token and save it as the default

.EXAMPLE
$session = New-ServiceNowSession -Url tenant.domain.com -Credential $mycred -ClientCredential $myClientCred -PassThru
Create a session using OAuth and save it as a local variable to be provided to functions directly

.EXAMPLE
New-ServiceNowSession -Url tenant.domain.com -Credential $mycred -Proxy http://1.2.3.4
Create a session utilizing a proxy to connect

.INPUTS
None

.OUTPUTS
Hashtable if -PassThru provided

.LINK
https://docs.servicenow.com/bundle/quebec-platform-administration/page/administer/security/reference/r_OAuthAPIRequestParameters.html
#>

function New-ServiceNowSession {

    [CmdletBinding(DefaultParameterSetName = 'BasicAuth')]
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'api call provides in plain text')]
    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state is actually changing')]

    param(
        [Parameter(Mandatory)]
        [Alias('ServiceNowUrl')]
        [string] $Url,

        [Parameter(Mandatory, ParameterSetName = 'BasicAuth')]
        [Parameter(Mandatory, ParameterSetName = 'OAuth')]
        [Parameter(Mandatory, ParameterSetName = 'BasicAuthProxy')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthProxy')]
        [Alias('Credentials')]
        [System.Management.Automation.PSCredential] $Credential,

        [Parameter(Mandatory, ParameterSetName = 'OAuth')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthProxy')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthClientCredential')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthClientCredentialProxy')]
        [System.Management.Automation.PSCredential] $ClientCredential,

        [Parameter(Mandatory, ParameterSetName = 'AccessToken')]
        [Parameter(Mandatory, ParameterSetName = 'AccessTokenProxy')]
        [string] $AccessToken,

        [Parameter(Mandatory, ParameterSetName = 'BasicAuthProxy')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthProxy')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthClientCredentialProxy')]
        [Parameter(Mandatory, ParameterSetName = 'AccessTokenProxy')]
        [string] $Proxy,

        [Parameter(ParameterSetName = 'BasicAuthProxy')]
        [Parameter(ParameterSetName = 'OAuthProxy')]
        [Parameter(ParameterSetName = 'OAuthClientCredentialProxy')]
        [Parameter(ParameterSetName = 'AccessTokenProxy')]
        [System.Management.Automation.PSCredential] $ProxyCredential,

        [Parameter()]
        [int] $ApiVersion,

        [Parameter()]
        [switch] $GetAllTable,

        [Parameter()]
        [switch] $GraphQL,

        [Parameter()]
        [ValidateRange(0, [int32]::MaxValue)]
        [int32] $TimeoutSec = 0,

        [Parameter()]
        [ValidateRange(0, 10)]
        [int] $RetryCount = 2,

        [Parameter()]
        [ValidateRange(1, 60)]
        [int] $RetryWaitSeconds = 5,

        [Parameter()]
        [ValidateRange(1, 300)]
        [int] $MaxRetryAfterSeconds = 10,

        [Parameter()]
        [switch] $PassThru
    )

    Write-Verbose $PSCmdLet.ParameterSetName

    if ( $ApiVersion -le 0 ) {
        $version = ''
    } else {
        $version = ('/v{0}' -f $ApiVersion)
    }

    $newSession = @{
        Domain              = $Url
        BaseUri             = ('https://{0}/api/' -f $Url)
        Version             = $version
        RetryCount          = $RetryCount
        RetryWaitSeconds    = $RetryWaitSeconds
        MaxRetryAfterSeconds = $MaxRetryAfterSeconds
    }

    if ( $GraphQL ) {
        $newSession.BaseUri = ('https://{0}/api/now/graphql' -f $Url)
    }

    if ( $PSBoundParameters.ContainsKey('Proxy') ) {
        $newSession.Add('Proxy', $Proxy)
        if ( $PSBoundParameters.ContainsKey('ProxyCredential') ) {
            $newSession.Add('ProxyCredential', $ProxyCredential)
        }
    }

    $script:PSDefaultParameterValues['Invoke-WebRequest:TimeoutSec'] = $TimeoutSec
    $script:PSDefaultParameterValues['Invoke-RestMethod:TimeoutSec'] = $TimeoutSec

    switch -Wildcard ($PSCmdLet.ParameterSetName) {
        'OAuth*' {
            # Determine OAuth grant type and build request body
            $oauthBody = @{
                'client_id'     = $ClientCredential.UserName
                'client_secret' = $ClientCredential.GetNetworkCredential().Password
            }

            if ($PSCmdLet.ParameterSetName -like 'OAuthClientCredential*') {
                # Client Credentials Grant (machine-to-machine)
                $oauthBody['grant_type'] = 'client_credentials'
                $grantType = 'client_credentials'
            }
            else {
                # Password Grant (user credentials)
                $oauthBody['grant_type'] = 'password'
                $oauthBody['username'] = $Credential.UserName
                $oauthBody['password'] = $Credential.GetNetworkCredential().Password
                $grantType = 'password'
            }

            $params = @{
                Uri             = 'https://{0}/oauth_token.do' -f $Url
                Body            = $oauthBody
                Method          = 'Post'
                UseBasicParsing = $true
            }

            # need to add this manually here, in addition to above, since we're making a rest call before our session is created
            if ( $PSBoundParameters.ContainsKey('Proxy') ) {
                $params.Add('Proxy', $Proxy)
                if ( $PSBoundParameters.ContainsKey('ProxyCredential') ) {
                    $params.Add('ProxyCredential', $ProxyCredential)
                } else {
                    $params.Add('ProxyUseDefaultCredentials', $true)
                }
            }

            $oldProgressPreference = $ProgressPreference
            $ProgressPreference = 'SilentlyContinue'

            $response = Invoke-WebRequest @params

            # set the progress pref back now that done with invoke-webrequest
            $ProgressPreference = $oldProgressPreference

            if ( $response.Content ) {
                $token = $response.Content | ConvertFrom-Json
                $newSession.Add('AccessToken', (New-Object System.Management.Automation.PSCredential('AccessToken', ($token.access_token | ConvertTo-SecureString -AsPlainText -Force))))
                
                # Password grant returns refresh_token, client credentials does not
                if ($token.refresh_token) {
                    $newSession.Add('RefreshToken', (New-Object System.Management.Automation.PSCredential('RefreshToken', ($token.refresh_token | ConvertTo-SecureString -AsPlainText -Force))))
                }

                if ($token.expires_in) {
                    $expiryTime = (Get-Date).AddSeconds($token.expires_in)
                    $newSession.Add('ExpiresOn', $expiryTime)
                    Write-Verbose "Access token will expire at $expiryTime"
                }

                # Store credentials and grant type for token refresh
                $newSession.Add('ClientCredential', $ClientCredential)
                $newSession.Add('GrantType', $grantType)
            } else {
                # invoke-webrequest didn't throw an error, but we didn't get a token back either
                throw ('{0} : {1}' -f $response.StatusCode, $response | Out-String )
            }
        }

        'AccessToken*' {
            $newSession.Add('AccessToken', (New-Object System.Management.Automation.PSCredential('AccessToken', ($AccessToken | ConvertTo-SecureString -AsPlainText -Force))))
        }

        'BasicAuth*' {
            $newSession.Add('Credential', $Credential)
        }

        Default {

        }
    }

    # Write-Verbose 'Retrieving list of classes for this instance. This will take a few seconds...'
    # $cmdbParams = @{
    # Table = 'sys_db_object'
    # # Query = 'nameSTARTSWITHcmdb_ci'
    # Properties = 'name', 'sys_id', 'label'
    # First = 100000
    # ServiceNowSession = $newSession
    # }

    # $class = Get-ServiceNowTable @cmdbParams -ErrorAction SilentlyContinue |
    # Select-Object @{
    # 'n' = 'Name'
    # 'e' = { $_.name }
    # },
    # @{
    # 'n' = 'SysId'
    # 'e' = { $_.sys_id }
    # },
    # @{
    # 'n' = 'ClassName'
    # 'e' = { $_.label }
    # }
    # if ( $class ) {
    # $newSession.Add('Classes', $class)
    # }

    Write-Verbose ($newSession | ConvertTo-Json)

    if ( $PassThru ) {
        $newSession
    } else {
        $Script:ServiceNowSession = $newSession
    }

    if ( $GetAllTable.IsPresent ) {
        Write-Verbose 'Getting table number prefixes'
        $defaultTable = $ServiceNowTable
        try {
            $numbers = Get-ServiceNowRecord -Table 'sys_number' -Property prefix, category -First 10000 -IncludeTotalCount
            foreach ($number in $numbers) {
                if ( $number.prefix.ToLower() -notin $defaultTable.NumberPrefix ) {
                    $ServiceNowTable.Add(
                        [pscustomobject] @{
                            "Name"             = ($number.category.link | Select-String -Pattern '^.*\?name=(.*)$').matches.groups[1].Value
                            "ClassName"        = $number.category.display_value
                            "Type"             = $null
                            "NumberPrefix"     = $number.prefix.ToLower()
                            "DescriptionField" = "short_description"
                        }
                    ) | Out-Null
                }
            }
        } catch {
            Write-Verbose "Session created, but failed to populate ServiceNowTable. Prefixes beyond the default won't be available. $_"
        }
    }
}