Private/Get-InforcerAuthHeader.ps1
|
function Get-InforcerAuthHeader { <# .SYNOPSIS Returns the authentication header for an Inforcer session (Private helper). .DESCRIPTION The only place the Inforcer auth header is built. API-key session (beta): @{ 'Inf-Api-Key' = <key> } Client-credentials session (v1): @{ Authorization = 'Bearer <token>' } For v1 the access token is requested when missing and renewed when less than five minutes remain; the new token and expiry are written back into the session hashtable. On failure it writes one error (NotConnected, EmptyApiKey or TokenRequestFailed) and returns nothing, so callers do: $h = Get-InforcerAuthHeader; if (-not $h) { return } .PARAMETER Session Session hashtable. Defaults to the current session. .PARAMETER ForceRefresh v1 only: request a new token even when the current one has not expired (used after a 401). #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $false)] [hashtable]$Session = $script:InforcerSession, [Parameter(Mandatory = $false)] [switch]$ForceRefresh ) if (-not $Session) { Write-Error -Message 'Not connected. Run Connect-Inforcer first.' -ErrorId 'NotConnected' -Category ConnectionError return } if ($Session.ApiVersion -eq 'v1') { $expiresAt = $Session.TokenExpiresAt -as [datetime] if ($ForceRefresh -or -not $Session.AccessToken -or -not $expiresAt -or $expiresAt -lt [datetime]::UtcNow.AddMinutes(5)) { $token = Get-InforcerAccessToken -ApiHost $Session.ApiHost -ClientId $Session.ClientId -ClientSecret $Session.ClientSecret if ($token.Error) { Write-Error -Message $token.Error -ErrorId 'TokenRequestFailed' -Category AuthenticationError return } $Session.AccessToken = $token.AccessToken $Session.TokenExpiresAt = $token.ExpiresAt } return @{ Authorization = 'Bearer ' + (ConvertFrom-InforcerSecureString -SecureString $Session.AccessToken) } } $apiKey = if ($Session.ApiKey -is [System.Security.SecureString]) { ConvertFrom-InforcerSecureString -SecureString $Session.ApiKey } if ([string]::IsNullOrWhiteSpace($apiKey)) { Write-Error -Message 'API key is empty or invalid. Please reconnect.' -ErrorId 'EmptyApiKey' -Category AuthenticationError return } @{ 'Inf-Api-Key' = $apiKey } } function Clear-InforcerRequestCredential { # A failed web cmdlet keeps the sent HttpRequestMessage as the ErrorRecord's TargetObject, and that same # object is referenced from $Error and any -ErrorVariable. Stripping the auth headers cleans every copy. param($ErrorRecord) $message = $ErrorRecord.TargetObject if ($message -is [System.Net.Http.HttpRequestMessage]) { $null = $message.Headers.Remove('Authorization') $null = $message.Headers.Remove('Inf-Api-Key') } } function Get-InforcerAccessToken { <# .SYNOPSIS Requests an OAuth client-credentials access token for the v1 API (Private helper). .DESCRIPTION POST {ApiHost}/oauth/token (form-encoded). Returns AccessToken (SecureString), ExpiresAt (UTC) and Error. On failure AccessToken is $null and Error holds a clean message that never contains the secret. Writes no errors itself, so the caller decides how to report. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The token arrives as plain text from the token endpoint and is stored only as a SecureString.')] [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [string]$ApiHost, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [System.Security.SecureString]$ClientSecret ) $secret = ConvertFrom-InforcerSecureString -SecureString $ClientSecret $request = @{ Uri = $ApiHost.TrimEnd('/') + '/oauth/token' Method = 'POST' Body = @{ grant_type = 'client_credentials'; client_id = $ClientId; client_secret = $secret } ContentType = 'application/x-www-form-urlencoded' Headers = @{ Accept = 'application/json' } TimeoutSec = 30 } $errorText = $null $response = $null try { $result = Invoke-InforcerHttpRequest -Request $request $response = $result.Body if ($result.StatusCode -ge 400) { $json = $response if ($json -is [string]) { try { $json = $json | ConvertFrom-Json -Depth 100 -ErrorAction Stop } catch { $json = $null } } $reason = $null if ($json -is [System.Management.Automation.PSCustomObject]) { $parts = foreach ($n in 'title', 'detail', 'error', 'error_description') { $v = $json.PSObject.Properties[$n].Value -as [string] if (-not [string]::IsNullOrWhiteSpace($v)) { $v.Trim().TrimEnd('.') } } $reason = @($parts | Select-Object -Unique) -join ': ' } if ([string]::IsNullOrWhiteSpace($reason)) { $reason = [string][System.Net.HttpStatusCode]$result.StatusCode } $hint = if ($result.StatusCode -in 400, 401) { ' Check the client ID and secret, and that the client is active and not expired.' } else { '' } $errorText = "Token request failed (HTTP $($result.StatusCode)): $reason.$hint" } } catch { Clear-InforcerRequestCredential -ErrorRecord $_ $errorText = "Token request failed: $($_.Exception.Message)" } $accessToken = $null if (-not $errorText) { $accessToken = $response.PSObject.Properties['access_token'].Value -as [string] if ([string]::IsNullOrWhiteSpace($accessToken)) { $errorText = 'Token request failed: the response contained no access_token.' } } if ($errorText) { return [PSCustomObject]@{ AccessToken = $null; ExpiresAt = $null; Error = (Protect-InforcerApiKeyInText -Text $errorText -ApiKey $secret -Session @{}) } } $lifetime = $response.PSObject.Properties['expires_in'].Value -as [int] if ($null -eq $lifetime -or $lifetime -le 0) { $lifetime = 3600 } [PSCustomObject]@{ AccessToken = ConvertTo-SecureString -String $accessToken -AsPlainText -Force ExpiresAt = [datetime]::UtcNow.AddSeconds($lifetime) Error = $null } } |