From fb997f8a6245e2171627e8cfc234d9d292a4b593 Mon Sep 17 00:00:00 2001 From: Jonathan Munro Date: Mon, 26 Jan 2026 11:08:01 +0000 Subject: [PATCH 1/3] Add support for authenticating with OAuth Client Credential Grant --- ServiceNow/Private/Get-ServiceNowAuth.ps1 | 63 ++++++++++++++------- ServiceNow/Public/New-ServiceNowSession.ps1 | 44 ++++++++++---- 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/ServiceNow/Private/Get-ServiceNowAuth.ps1 b/ServiceNow/Private/Get-ServiceNowAuth.ps1 index b0afa3b..4f652fc 100644 --- a/ServiceNow/Private/Get-ServiceNowAuth.ps1 +++ b/ServiceNow/Private/Get-ServiceNowAuth.ps1 @@ -38,31 +38,52 @@ function Get-ServiceNowAuth { if ($ServiceNowSession.Version) { $hashOut.uri = $hashOut.uri + $ServiceNowSession.Version } # check if we need a new access token - if ( $ServiceNowSession.ExpiresOn -lt (Get-Date) -and $ServiceNowSession.RefreshToken -and $ServiceNowSession.ClientCredential ) { - # we've expired and have a refresh token - $refreshParams = @{ - Uri = 'https://{0}/oauth_token.do' -f $ServiceNowSession.Domain - Method = 'POST' - ContentType = 'application/x-www-form-urlencoded' - Body = @{ - grant_type = 'refresh_token' - client_id = $ServiceNowSession.ClientCredential.UserName - client_secret = $ServiceNowSession.ClientCredential.GetNetworkCredential().password - refresh_token = $ServiceNowSession.RefreshToken.GetNetworkCredential().password - } + if ( $ServiceNowSession.ExpiresOn -lt (Get-Date) -and $ServiceNowSession.ClientCredential ) { + + # Build refresh/re-auth body based on grant type + $refreshBody = @{ + client_id = $ServiceNowSession.ClientCredential.UserName + client_secret = $ServiceNowSession.ClientCredential.GetNetworkCredential().password } - $response = Invoke-RestMethod @refreshParams - - $ServiceNowSession.AccessToken = New-Object System.Management.Automation.PSCredential('AccessToken', ($response.access_token | ConvertTo-SecureString -AsPlainText -Force)) - $ServiceNowSession.RefreshToken = New-Object System.Management.Automation.PSCredential('RefreshToken', ($response.refresh_token | ConvertTo-SecureString -AsPlainText -Force)) - if ($response.expires_in) { - $ServiceNowSession.ExpiresOn = (Get-Date).AddSeconds($response.expires_in) - Write-Verbose ('Access token has been refreshed and will expire at {0}' -f $ServiceNowSession.ExpiresOn) + if ($ServiceNowSession.GrantType -eq 'client_credentials') { + # Client credentials: re-authenticate (no refresh token available) + $refreshBody['grant_type'] = 'client_credentials' + } + elseif ($ServiceNowSession.RefreshToken) { + # Password grant: use refresh token + $refreshBody['grant_type'] = 'refresh_token' + $refreshBody['refresh_token'] = $ServiceNowSession.RefreshToken.GetNetworkCredential().password } + else { + Write-Warning 'Access token expired but no refresh method available' + } + + if ($refreshBody.ContainsKey('grant_type')) { + $refreshParams = @{ + Uri = 'https://{0}/oauth_token.do' -f $ServiceNowSession.Domain + Method = 'POST' + ContentType = 'application/x-www-form-urlencoded' + Body = $refreshBody + } - # ensure script/module scoped variable is updated - $script:ServiceNowSession = $ServiceNowSession + $response = Invoke-RestMethod @refreshParams + + $ServiceNowSession.AccessToken = New-Object System.Management.Automation.PSCredential('AccessToken', ($response.access_token | ConvertTo-SecureString -AsPlainText -Force)) + + # Update refresh token if provided (password grant only) + if ($response.refresh_token) { + $ServiceNowSession.RefreshToken = New-Object System.Management.Automation.PSCredential('RefreshToken', ($response.refresh_token | ConvertTo-SecureString -AsPlainText -Force)) + } + + if ($response.expires_in) { + $ServiceNowSession.ExpiresOn = (Get-Date).AddSeconds($response.expires_in) + Write-Verbose ('Access token has been refreshed and will expire at {0}' -f $ServiceNowSession.ExpiresOn) + } + + # ensure script/module scoped variable is updated + $script:ServiceNowSession = $ServiceNowSession + } } if ( $ServiceNowSession.AccessToken ) { diff --git a/ServiceNow/Public/New-ServiceNowSession.ps1 b/ServiceNow/Public/New-ServiceNowSession.ps1 index dca4e3b..a0a1b60 100644 --- a/ServiceNow/Public/New-ServiceNowSession.ps1 +++ b/ServiceNow/Public/New-ServiceNowSession.ps1 @@ -101,6 +101,7 @@ function New-ServiceNowSession { [Parameter(Mandatory, ParameterSetName = 'OAuth')] [Parameter(Mandatory, ParameterSetName = 'OAuthProxy')] + [Parameter(Mandatory, ParameterSetName = 'OAuthClientCredential')] [System.Management.Automation.PSCredential] $ClientCredential, [Parameter(Mandatory, ParameterSetName = 'AccessToken')] @@ -160,19 +161,32 @@ function New-ServiceNowSession { } $script:PSDefaultParameterValues['Invoke-WebRequest:TimeoutSec'] = $TimeoutSec - $script:PSDefaultParameterValues['Invoke-RestMethodt: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 -eq '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 = @{ - 'grant_type' = 'password' - 'client_id' = $ClientCredential.UserName - 'client_secret' = $ClientCredential.GetNetworkCredential().Password - 'username' = $Credential.UserName - 'password' = $Credential.GetNetworkCredential().Password - } + Body = $oauthBody Method = 'Post' UseBasicParsing = $true } @@ -198,18 +212,24 @@ function New-ServiceNowSession { 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)))) - $newSession.Add('RefreshToken', (New-Object System.Management.Automation.PSCredential('RefreshToken', ($token.refresh_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 client credential as it will be needed to refresh the access token - $newSession.Add('ClientCredential', $ClientCredential) + # 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 ) + throw ('{0} : {1}' -f $response.StatusCode, $response | Out-String ) } } From 605c45bf73436d18786bc1a9d5083b72355529a6 Mon Sep 17 00:00:00 2001 From: Greg Brownstein Date: Sat, 18 Jul 2026 10:44:45 -0400 Subject: [PATCH 2/3] Add OAuthClientCredentialProxy paramset and tests for PR #292 Builds on jmunroBT's OAuth client_credentials grant work (PR #292): - Add OAuthClientCredentialProxy parameter set to New-ServiceNowSession so -ClientCredential can be combined with -Proxy/-ProxyCredential without requiring -Credential, as requested in review. - Fix grant type detection to match on 'OAuthClientCredential*' instead of an exact string, since the new proxy paramset has a different ParameterSetName. Without this fix, using -ClientCredential with -Proxy fell through to the password grant branch and threw a null reference exception on the missing -Credential. - Add Pester tests covering client_credentials grant (with and without proxy), password grant, and Get-ServiceNowAuth token refresh logic for both grant types. --- ServiceNow/Public/New-ServiceNowSession.ps1 | 15 +- Tests/OAuthClientCredential.Tests.ps1 | 186 ++++++++++++++++++++ 2 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 Tests/OAuthClientCredential.Tests.ps1 diff --git a/ServiceNow/Public/New-ServiceNowSession.ps1 b/ServiceNow/Public/New-ServiceNowSession.ps1 index a0a1b60..6724ebe 100644 --- a/ServiceNow/Public/New-ServiceNowSession.ps1 +++ b/ServiceNow/Public/New-ServiceNowSession.ps1 @@ -15,10 +15,12 @@ If using OAuth, the client credential will be stored in the script scoped variab 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. +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. @@ -58,7 +60,11 @@ Use GraphQL instead of REST. .EXAMPLE New-ServiceNowSession -Url tenant.domain.com -Credential $mycred -ClientCredential $myClientCred -Create a session using OAuth and save it as the default +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' @@ -102,6 +108,7 @@ function New-ServiceNowSession { [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')] @@ -110,11 +117,13 @@ function New-ServiceNowSession { [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, @@ -171,7 +180,7 @@ function New-ServiceNowSession { 'client_secret' = $ClientCredential.GetNetworkCredential().Password } - if ($PSCmdLet.ParameterSetName -eq 'OAuthClientCredential') { + if ($PSCmdLet.ParameterSetName -like 'OAuthClientCredential*') { # Client Credentials Grant (machine-to-machine) $oauthBody['grant_type'] = 'client_credentials' $grantType = 'client_credentials' diff --git a/Tests/OAuthClientCredential.Tests.ps1 b/Tests/OAuthClientCredential.Tests.ps1 new file mode 100644 index 0000000..4ce2d3c --- /dev/null +++ b/Tests/OAuthClientCredential.Tests.ps1 @@ -0,0 +1,186 @@ +$ProjectRoot = Resolve-Path "$PSScriptRoot/.." +$ModulePsd = (Resolve-Path "$ProjectRoot/ServiceNow/ServiceNow.psd1").Path + +Get-Module 'ServiceNow' | Remove-Module -Force -ErrorAction SilentlyContinue +Import-Module $ModulePsd -Force + +Describe 'New-ServiceNowSession OAuth Client Credentials Grant' { + + BeforeAll { + $script:testClientCredential = [PSCredential]::new('myClientId', ('myClientSecret' | ConvertTo-SecureString -AsPlainText -Force)) + } + + Context 'Client credentials grant without proxy' { + + It 'Authenticates using client_credentials grant type' { + Mock Invoke-WebRequest -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + StatusCode = 200 + Content = '{"access_token": "abc123", "expires_in": 1800}' + } + } + + $session = New-ServiceNowSession -Url 'test.service-now.com' -ClientCredential $script:testClientCredential -PassThru + + $session.GrantType | Should -Be 'client_credentials' + $session.AccessToken | Should -Not -BeNullOrEmpty + $session.RefreshToken | Should -BeNullOrEmpty + $session.ClientCredential | Should -Be $script:testClientCredential + + Should -Invoke Invoke-WebRequest -ModuleName 'ServiceNow' -Times 1 -Exactly -ParameterFilter { + $Body.grant_type -eq 'client_credentials' + } + } + } + + Context 'Client credentials grant with proxy' { + + It 'Accepts ClientCredential and Proxy without requiring Credential' { + Mock Invoke-WebRequest -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + StatusCode = 200 + Content = '{"access_token": "abc123", "expires_in": 1800}' + } + } + + $session = New-ServiceNowSession -Url 'test.service-now.com' -ClientCredential $script:testClientCredential -Proxy 'http://proxy.example.com:8080' -PassThru + + $session.GrantType | Should -Be 'client_credentials' + $session.Proxy | Should -Be 'http://proxy.example.com:8080' + + Should -Invoke Invoke-WebRequest -ModuleName 'ServiceNow' -Times 1 -Exactly -ParameterFilter { + $Proxy -eq 'http://proxy.example.com:8080' + } + } + + It 'Accepts ClientCredential, Proxy, and ProxyCredential together' { + Mock Invoke-WebRequest -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + StatusCode = 200 + Content = '{"access_token": "abc123", "expires_in": 1800}' + } + } + + $proxyCred = [PSCredential]::new('proxyuser', ('proxypass' | ConvertTo-SecureString -AsPlainText -Force)) + + { New-ServiceNowSession -Url 'test.service-now.com' -ClientCredential $script:testClientCredential -Proxy 'http://proxy.example.com:8080' -ProxyCredential $proxyCred -PassThru } | Should -Not -Throw + } + } + + Context 'Password grant still works alongside client credentials support' { + + It 'Authenticates using password grant type when Credential and ClientCredential are both provided' { + Mock Invoke-WebRequest -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + StatusCode = 200 + Content = '{"access_token": "abc123", "refresh_token": "refresh456", "expires_in": 1800}' + } + } + + $userCred = [PSCredential]::new('user', ('pass' | ConvertTo-SecureString -AsPlainText -Force)) + + $session = New-ServiceNowSession -Url 'test.service-now.com' -Credential $userCred -ClientCredential $script:testClientCredential -PassThru + + $session.GrantType | Should -Be 'password' + $session.RefreshToken | Should -Not -BeNullOrEmpty + + Should -Invoke Invoke-WebRequest -ModuleName 'ServiceNow' -Times 1 -Exactly -ParameterFilter { + $Body.grant_type -eq 'password' + } + } + } +} + +Describe 'Get-ServiceNowAuth Token Refresh Logic' { + + Context 'Client credentials grant refresh' { + + It 'Re-authenticates using client_credentials when token expired and no refresh token exists' { + Mock Invoke-RestMethod -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + access_token = 'newtoken123' + expires_in = 1800 + } + } + + $session = @{ + Domain = 'test.service-now.com' + BaseUri = 'https://test.service-now.com/api/' + Version = '' + ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) + GrantType = 'client_credentials' + ExpiresOn = (Get-Date).AddMinutes(-5) + } + + InModuleScope 'ServiceNow' -Parameters @{ session = $session } { + param($session) + $result = Get-ServiceNowAuth -ServiceNowSession $session + $result | Should -Not -BeNullOrEmpty + $session.AccessToken | Should -Not -BeNullOrEmpty + } + + Should -Invoke Invoke-RestMethod -ModuleName 'ServiceNow' -Times 1 -Exactly -ParameterFilter { + $Body.grant_type -eq 'client_credentials' + } + } + } + + Context 'Password grant refresh' { + + It 'Uses refresh_token grant when a refresh token is available' { + Mock Invoke-RestMethod -ModuleName 'ServiceNow' { + [PSCustomObject]@{ + access_token = 'newtoken123' + refresh_token = 'newrefresh456' + expires_in = 1800 + } + } + + $session = @{ + Domain = 'test.service-now.com' + BaseUri = 'https://test.service-now.com/api/' + Version = '' + ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) + RefreshToken = [PSCredential]::new('RefreshToken', ('oldrefresh' | ConvertTo-SecureString -AsPlainText -Force)) + GrantType = 'password' + ExpiresOn = (Get-Date).AddMinutes(-5) + } + + InModuleScope 'ServiceNow' -Parameters @{ session = $session } { + param($session) + $null = Get-ServiceNowAuth -ServiceNowSession $session + $session.RefreshToken | Should -Not -BeNullOrEmpty + } + + Should -Invoke Invoke-RestMethod -ModuleName 'ServiceNow' -Times 1 -Exactly -ParameterFilter { + $Body.grant_type -eq 'refresh_token' + } + } + } + + Context 'No refresh method available' { + + It 'Warns and does not call Invoke-RestMethod when no refresh token or client credentials grant type exists' { + Mock Invoke-RestMethod -ModuleName 'ServiceNow' {} + Mock Write-Warning -ModuleName 'ServiceNow' {} + + $session = @{ + Domain = 'test.service-now.com' + BaseUri = 'https://test.service-now.com/api/' + Version = '' + ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) + AccessToken = [PSCredential]::new('AccessToken', ('staletoken' | ConvertTo-SecureString -AsPlainText -Force)) + GrantType = 'password' + ExpiresOn = (Get-Date).AddMinutes(-5) + } + + InModuleScope 'ServiceNow' -Parameters @{ session = $session } { + param($session) + $null = Get-ServiceNowAuth -ServiceNowSession $session + } + + Should -Invoke Invoke-RestMethod -ModuleName 'ServiceNow' -Times 0 + Should -Invoke Write-Warning -ModuleName 'ServiceNow' -Times 1 + } + } +} From 1f3909f3c55ac9fe7baa594c88a1307a34e745f2 Mon Sep 17 00:00:00 2001 From: Greg Brownstein Date: Sat, 18 Jul 2026 11:52:37 -0400 Subject: [PATCH 3/3] Fix PSScriptAnalyzer error: avoid ConvertTo-SecureString -AsPlainText in tests --- Tests/OAuthClientCredential.Tests.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/OAuthClientCredential.Tests.ps1 b/Tests/OAuthClientCredential.Tests.ps1 index 4ce2d3c..107435e 100644 --- a/Tests/OAuthClientCredential.Tests.ps1 +++ b/Tests/OAuthClientCredential.Tests.ps1 @@ -7,7 +7,7 @@ Import-Module $ModulePsd -Force Describe 'New-ServiceNowSession OAuth Client Credentials Grant' { BeforeAll { - $script:testClientCredential = [PSCredential]::new('myClientId', ('myClientSecret' | ConvertTo-SecureString -AsPlainText -Force)) + $script:testClientCredential = [PSCredential]::new('myClientId', ([System.Net.NetworkCredential]::new('', 'myClientSecret').SecurePassword)) } Context 'Client credentials grant without proxy' { @@ -61,7 +61,7 @@ Describe 'New-ServiceNowSession OAuth Client Credentials Grant' { } } - $proxyCred = [PSCredential]::new('proxyuser', ('proxypass' | ConvertTo-SecureString -AsPlainText -Force)) + $proxyCred = [PSCredential]::new('proxyuser', ([System.Net.NetworkCredential]::new('', 'proxypass').SecurePassword)) { New-ServiceNowSession -Url 'test.service-now.com' -ClientCredential $script:testClientCredential -Proxy 'http://proxy.example.com:8080' -ProxyCredential $proxyCred -PassThru } | Should -Not -Throw } @@ -77,7 +77,7 @@ Describe 'New-ServiceNowSession OAuth Client Credentials Grant' { } } - $userCred = [PSCredential]::new('user', ('pass' | ConvertTo-SecureString -AsPlainText -Force)) + $userCred = [PSCredential]::new('user', ([System.Net.NetworkCredential]::new('', 'pass').SecurePassword)) $session = New-ServiceNowSession -Url 'test.service-now.com' -Credential $userCred -ClientCredential $script:testClientCredential -PassThru @@ -107,7 +107,7 @@ Describe 'Get-ServiceNowAuth Token Refresh Logic' { Domain = 'test.service-now.com' BaseUri = 'https://test.service-now.com/api/' Version = '' - ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) + ClientCredential = [PSCredential]::new('clientid', ([System.Net.NetworkCredential]::new('', 'secret').SecurePassword)) GrantType = 'client_credentials' ExpiresOn = (Get-Date).AddMinutes(-5) } @@ -140,8 +140,8 @@ Describe 'Get-ServiceNowAuth Token Refresh Logic' { Domain = 'test.service-now.com' BaseUri = 'https://test.service-now.com/api/' Version = '' - ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) - RefreshToken = [PSCredential]::new('RefreshToken', ('oldrefresh' | ConvertTo-SecureString -AsPlainText -Force)) + ClientCredential = [PSCredential]::new('clientid', ([System.Net.NetworkCredential]::new('', 'secret').SecurePassword)) + RefreshToken = [PSCredential]::new('RefreshToken', ([System.Net.NetworkCredential]::new('', 'oldrefresh').SecurePassword)) GrantType = 'password' ExpiresOn = (Get-Date).AddMinutes(-5) } @@ -168,8 +168,8 @@ Describe 'Get-ServiceNowAuth Token Refresh Logic' { Domain = 'test.service-now.com' BaseUri = 'https://test.service-now.com/api/' Version = '' - ClientCredential = [PSCredential]::new('clientid', ('secret' | ConvertTo-SecureString -AsPlainText -Force)) - AccessToken = [PSCredential]::new('AccessToken', ('staletoken' | ConvertTo-SecureString -AsPlainText -Force)) + ClientCredential = [PSCredential]::new('clientid', ([System.Net.NetworkCredential]::new('', 'secret').SecurePassword)) + AccessToken = [PSCredential]::new('AccessToken', ([System.Net.NetworkCredential]::new('', 'staletoken').SecurePassword)) GrantType = 'password' ExpiresOn = (Get-Date).AddMinutes(-5) }