Private/Get-MyWebApiToken.ps1
|
function Get-MyWebApiToken { # Returns a valid bearer token, refreshing via client_credentials if expired. # RefreshGate is per connection: concurrent callers for one target single-flight, # while independent targets never share credentials or refresh state. [CmdletBinding()] param([object] $Connection) $ctx = Resolve-MyWebApiContext -Connection $Connection if ([bool](Get-MyWebApiContextProperty -Context $ctx -Name 'Disposed')) { throw 'The supplied MyWebApi connection has been disconnected.' } $get = { param([string]$name) Get-MyWebApiContextProperty -Context $ctx -Name $name } $token = & $get 'AccessToken' $expiresAt = & $get 'ExpiresAt' if ($token -and $expiresAt -and ($expiresAt -gt [DateTimeOffset]::UtcNow.AddSeconds(30))) { return $token } if (-not (& $get 'ClientId')) { # A pre-obtained token has no refresh credentials. Let the server report expiry. return $token } $gate = & $get 'RefreshGate' if ($null -eq $gate) { $gate = [System.Threading.SemaphoreSlim]::new(1, 1) Set-MyWebApiContextProperty -Context $ctx -Name 'RefreshGate' -Value $gate } $timeout = Get-MyWebApiTimeoutSeconds -Context $ctx $entered = $false try { if (-not $gate.Wait($timeout * 1000)) { throw "Timed out waiting for OAuth token refresh after $timeout second(s)." } $entered = $true # Another caller may have refreshed while this caller waited. $token = & $get 'AccessToken' $expiresAt = & $get 'ExpiresAt' if ($token -and $expiresAt -and ($expiresAt -gt [DateTimeOffset]::UtcNow.AddSeconds(30))) { return $token } $authority = & $get 'Authority' if (-not $authority) { throw 'Authority is required for token refresh.' } $authorityUri = Assert-MyWebApiEndpoint -Value $authority -Name 'Authority' -Context $ctx $discoveryUri = '{0}/.well-known/openid-configuration' -f $authorityUri.AbsoluteUri.TrimEnd('/') Assert-MyWebApiNotCancelled -Context $ctx $discovery = Invoke-MyWebApiHttpJson -Context $ctx -Method Get -Uri $discoveryUri $tokenEndpointValue = Get-MyWebApiContextProperty -Context $discovery -Name 'token_endpoint' if (-not $tokenEndpointValue) { throw 'OAuth discovery did not return token_endpoint.' } $tokenEndpoint = Assert-MyWebApiEndpoint -Value ([string]$tokenEndpointValue) -Name 'token_endpoint' -Context $ctx $authorityOrigin = Get-MyWebApiOrigin -Uri $authorityUri $trustedValue = & $get 'TrustedTokenEndpoint' $trustedUri = $null if ($trustedValue) { $trustedUri = Assert-MyWebApiEndpoint -Value ([string]$trustedValue) -Name 'TrustedTokenEndpoint' -Context $ctx } $sameOrigin = [string]::Equals((Get-MyWebApiOrigin -Uri $tokenEndpoint), $authorityOrigin, [StringComparison]::OrdinalIgnoreCase) $explicitTrust = $trustedUri -and [string]::Equals($tokenEndpoint.AbsoluteUri.TrimEnd('/'), $trustedUri.AbsoluteUri.TrimEnd('/'), [StringComparison]::OrdinalIgnoreCase) if (-not $sameOrigin -and -not $explicitTrust) { throw 'OAuth discovery returned a token_endpoint outside Authority; configure an exact TrustedTokenEndpoint to opt in.' } $bstr = [IntPtr]::Zero $plainSecret = $null try { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR((& $get 'ClientSecret')) $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) $body = @{ grant_type = 'client_credentials' client_id = (& $get 'ClientId') client_secret = $plainSecret scope = (& $get 'Scope') } Assert-MyWebApiNotCancelled -Context $ctx $response = Invoke-MyWebApiHttpJson -Context $ctx -Method Post -Uri $tokenEndpoint.AbsoluteUri -FormBody $body } finally { if ($bstr -ne [IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $plainSecret = $null } $newToken = Get-MyWebApiContextProperty -Context $response -Name 'access_token' if (-not $newToken) { throw 'OAuth token response did not contain access_token.' } $expiresIn = Get-MyWebApiContextProperty -Context $response -Name 'expires_in' if ($null -eq $expiresIn -or [int]$expiresIn -lt 1) { $expiresIn = 300 } Set-MyWebApiContextProperty -Context $ctx -Name 'AccessToken' -Value ([string]$newToken) Set-MyWebApiContextProperty -Context $ctx -Name 'ExpiresAt' -Value ([DateTimeOffset]::UtcNow.AddSeconds([int]$expiresIn)) return [string]$newToken } finally { if ($entered) { [void]$gate.Release() } } } |