Providers/PingOne/Public/Get-PingOneAccessToken.ps1
|
function Get-PingOneAccessToken { <# .SYNOPSIS Returns a live PingOne access token, renewing it when it is about to expire .DESCRIPTION PingOne issues a worker application an access token that lives an hour. That is longer than most steps and shorter than a seed of any size, so a token fetched at connect time and never looked at again produces a 401 partway through a run - typically during teardown, which is the worst moment for a run to stop. The token is therefore renewed here rather than by any caller, a minute before it expires, and the renewed value is written back onto the connection so the next call finds it already fresh. The token endpoint belongs to the environment the worker application LIVES in, which is not necessarily the environment being seeded. Posting to the target environment's endpoint with a worker from elsewhere is refused with `invalid_client`, a message that reads like a disabled application or a bad secret and is neither. That is why the connection carries AuthEnvironmentId separately, and why this function uses it. The secret is held on the connection as a SecureString and converted for exactly as long as the request takes. .PARAMETER AsPlainText Return the bare token string rather than an object. Used by the request function, which needs it for an Authorization header. .PARAMETER Connection The connection to use. Defaults to the session's. .OUTPUTS PSCustomObject describing the token, or System.String with -AsPlainText. .EXAMPLE PS> Get-TestAccessToken DESCRIPTION: Returns the current token and when it expires OUTPUT: An object with ExpiresUtc and the scopes the token carries USE CASE: Checking that a connection is still good before a long run .EXAMPLE PS> $bearer = Get-PingOneAccessToken -AsPlainText DESCRIPTION: Fetches the raw token for an Authorization header OUTPUT: The token string USE CASE: Called by Invoke-PingOneRequest on every request .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [switch]$AsPlainText, [Parameter()] [hashtable]$Connection ) if (-not $Connection) { $Connection = Get-PingOneConnection } $needsToken = ( [string]::IsNullOrWhiteSpace($Connection.AccessToken) -or -not $Connection.TokenExpiresUtc -or [DateTime]::UtcNow -ge $Connection.TokenExpiresUtc.AddSeconds(-60) ) if ($needsToken) { Write-Verbose 'Requesting a PingOne access token' $uri = 'https://{0}/{1}/as/token' -f $Connection.AuthHost, $Connection.AuthEnvironmentId # Basic, because that is what a worker application is created with. The secret is # materialised here and nowhere else, and only for the length of the call. $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Connection.ClientSecret) try { $secret = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) $pair = '{0}:{1}' -f $Connection.ClientId, $secret $basic = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair)) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $requestedAt = [DateTime]::UtcNow # Encoding, TLS and the progress bar are Invoke-TestWebRequest's job, the same as for every # other call this module makes. try { $response = Invoke-TestWebRequest -Method POST -Uri $uri ` -Headers @{ Authorization = "Basic $basic" } ` -Body 'grant_type=client_credentials' ` -ContentType 'application/x-www-form-urlencoded' $payload = $response.Content | ConvertFrom-Json } catch { $detail = Get-PingOneErrorDetail -ErrorRecord $_ # Formatted as one string first. Written as 'a {0}' + 'b' + 'c' -f ..., the -f binds # to the last literal only, because it binds tighter than +, and the message went out # with its placeholders unfilled. $template = 'Could not get a PingOne access token from {0}: {1}. An invalid_client here ' + 'usually means the worker application lives in a different environment from the ' + 'one named by -AuthEnvironmentId, or that it is disabled.' throw ($template -f $uri, $detail.Summary) } finally { $basic = $null $pair = $null $secret = $null } $Connection.AccessToken = $payload.access_token $Connection.TokenExpiresUtc = $requestedAt.AddSeconds([int]$payload.expires_in) $Connection.TokenScopes = @(($payload.scope -split '\s+') | Where-Object { $_ }) } if ($AsPlainText) { return $Connection.AccessToken } return [PSCustomObject]@{ PSTypeName = 'PingOneAccessToken' EnvironmentId = $Connection.EnvironmentId AuthEnvironmentId = $Connection.AuthEnvironmentId ClientId = $Connection.ClientId ExpiresUtc = $Connection.TokenExpiresUtc ExpiresInSeconds = [int]([Math]::Max(0, ($Connection.TokenExpiresUtc - [DateTime]::UtcNow).TotalSeconds)) Scopes = $Connection.TokenScopes } } |