Private/Get-UKGHRSDAccessToken.ps1

function Get-UKGHRSDAccessToken {
    <#
    .SYNOPSIS
        Requests an OAuth application access_token from the HRSD /tokens endpoint.

    .DESCRIPTION
        Implements the client_credentials flow documented in the HRSD OAuth guide:
        POST {BaseUrl}/api/v2/client/tokens
          - Basic auth with ApplicationId:ApplicationSecret
          - body: grant_type=client_credentials&scope=client&client_id=<ClientId>

        Returns a PSCustomObject with the token and its computed expiry so the
        caller (Connect-UKGHRSD) can store it and refresh proactively before it
        expires, as UKG recommends.

    .NOTES
        Internal helper. Not exported. The returned token is an *application*
        token for server-to-server use only and must never be surfaced to an
        end-user device.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param (
        [Parameter(Mandatory)]
        [string]$BaseUrl,

        [Parameter(Mandatory)]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [string]$ApplicationSecret,

        [Parameter(Mandatory)]
        [string]$ClientId
    )

    $tokenUri = "$BaseUrl/api/v2/client/tokens"

    # Build the Basic auth header from application_id:application_secret.
    $pair    = "{0}:{1}" -f $ApplicationId, $ApplicationSecret
    $basic   = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
    $headers = @{
        Authorization = "Basic $basic"
        Accept        = 'application/json'
    }

    # client_credentials grant. scope is 'client' for the client-scope API.
    $body = @{
        grant_type = 'client_credentials'
        scope      = 'client'
        client_id  = $ClientId
    }

    Write-Verbose "Requesting access token from $tokenUri"

    try {
        # Capture the moment just before the call so expiry math stays conservative.
        $requestedAt = Get-Date
        $response = Invoke-RestMethod -Method Post -Uri $tokenUri `
            -Headers $headers -Body $body `
            -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop
    }
    catch {
        # Surface UKG's response body (OAuth 2.0 error / description) rather
        # than the bare HTTP status — the difference between invalid_client,
        # invalid_grant, and a wrong base URL is impossible to debug otherwise.
        $detail = Get-UKGHRSDErrorMessage -ErrorRecord $_
        throw "UKG HRSD token request to $tokenUri failed. $detail`nVerify -Region, -ClientId, and the application_id / application_secret with your UKG IPM contact (credentials are per-environment; staging and production credentials are not interchangeable)."
    }

    if (-not $response.access_token) {
        throw "UKG HRSD token endpoint returned no access_token. Response: $($response | ConvertTo-Json -Compress)"
    }

    [pscustomobject]@{
        AccessToken = $response.access_token
        TokenType   = $response.token_type
        ExpiresIn   = $response.expires_in
        # Store an absolute expiry so refresh logic doesn't have to track durations.
        ExpiresAt   = $requestedAt.AddSeconds([int]$response.expires_in)
    }
}