From fb997f8a6245e2171627e8cfc234d9d292a4b593 Mon Sep 17 00:00:00 2001 From: Jonathan Munro Date: Mon, 26 Jan 2026 11:08:01 +0000 Subject: [PATCH] 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 ) } }