Api/ConfideCommon.ps1
|
function Get-IdentityServiceToken { [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory)] [string]$IdentityServiceUri, [Parameter(Mandatory)] [string]$Scope, [Parameter(Mandatory)] [string]$ClientId, [Parameter(Mandatory)] [Alias("Certificate", "Cert")] [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate ) PROCESS { 'Calling method: Get-IdentityServiceToken' | Write-Debug $encodedThumbprint = ConvertTo-Base64UrlEncodedString -Bytes $SigningCertificate.GetCertHash() $headerTable = [ordered]@{typ = "JWT"; alg = "RS256"; kid = $encodedThumbprint } $header = $headerTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString $now = Get-Date $currentEpochTime = Convert-DateTimeToEpoch -DateTime $now $notBefore = $currentEpochTime $futureEpochTime = Convert-DateTimeToEpoch -DateTime ($now.AddHours(1)) $payloadTable = [ordered]@{sub = $ClientId; jti = ([System.Guid]::NewGuid()).ToString(); iss = $ClientId; aud = $IdentityServiceUri.TrimEnd('/') + "/connect/token"; nbf = $notBefore; exp = $futureEpochTime; iat = $currentEpochTime } $payload = $payloadTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString $jwtPlainText = "{0}.{1}" -f $header, $payload $jwtSig = New-JwtRsaSignature -JsonWebToken $jwtPlainText -SigningCertificate $SigningCertificate $ClientAssertion = "{0}.{1}" -f $jwtPlainText, $jwtSig $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token" $Body = @{ grant_type = 'client_credentials' scope = $Scope client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' client_assertion = $ClientAssertion } $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing return (ConvertFrom-Json $Response).access_token } } function Get-IdentityServiceTokenByClientSecret { [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory)] [string]$IdentityServiceUri, [Parameter(Mandatory)] [string]$Scope, [Parameter(Mandatory)] [string]$ClientId, [Parameter(Mandatory)] [string]$ClientSecret ) PROCESS { 'Calling method: Get-IdentityServiceTokenByClientSecret' | Write-Debug $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token" $Body = @{ grant_type = 'client_credentials' scope = $Scope client_id = $ClientId client_secret = $ClientSecret } $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing return (ConvertFrom-Json $Response).access_token } } function New-JwtRsaSignature { [CmdletBinding()] [OutputType([string])] Param( [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate, [String]$JsonWebToken ) PROCESS { 'Calling method: New-JwtRsaSignature' | Write-Debug $rsa = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($SigningCertificate)) if ($null -eq $rsa) { # Requiring the private key to be present throw "There's no private key in the supplied certificate." } [byte[]]$message = [System.Text.Encoding]::UTF8.GetBytes($JsonWebToken) $sigBytes = $rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) return ConvertTo-Base64UrlEncodedString -Bytes $sigBytes } } function ConvertTo-Base64UrlEncodedString { [CmdletBinding()] [OutputType([string])] Param ( [Parameter(Position = 0, ParameterSetName = "String", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$InputString, [Parameter(Position = 1, ParameterSetName = "Byte Array", Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] [byte[]]$Bytes ) PROCESS { [string]$base64UrlEncodedString = "" if ($PSBoundParameters.ContainsKey("Bytes")) { $output = [Convert]::ToBase64String($Bytes) $output = $output.Split('=')[0] # Remove any trailing '='s $output = $output.Replace('+', '-') # 62nd char of encoding $output = $output.Replace('/', '_') # 63rd char of encoding $base64UrlEncodedString = $output } else { $encoder = [System.Text.UTF8Encoding]::new() [byte[]]$inputBytes = $encoder.GetBytes($InputString) $base64String = [Convert]::ToBase64String($inputBytes) [string]$base64UrlEncodedString = "" $base64UrlEncodedString = $base64String.Split('=')[0] # Remove any trailing '='s $base64UrlEncodedString = $base64UrlEncodedString.Replace('+', '-'); # 62nd char of encoding $base64UrlEncodedString = $base64UrlEncodedString.Replace('/', '_'); # 63rd char of encoding } return $base64UrlEncodedString } } function Convert-DateTimeToEpoch { [CmdletBinding()] [OutputType([System.Int64])] Param( [Parameter(Mandatory)] [DateTime]$DateTime ) PROCESS { 'Calling method: Convert-DateTimeToEpoch' | Write-Debug $dtut = $DateTime.ToUniversalTime() [TimeSpan]$ts = New-TimeSpan -Start (Get-Date "01/01/1970") -End $dtut [Int64]$secondsSinceEpoch = [Math]::Floor($ts.TotalSeconds) return $secondsSinceEpoch } } function Convert-JWTtoken { [CmdletBinding()] [OutputType([System.Int64])] Param( [Parameter(Mandatory = $true)] [String]$token ) PROCESS { 'Calling method: Convert-JWTtoken' | Write-Debug #Validate as per https://tools.ietf.org/html/rfc7519 #Access and ID tokens are fine, Refresh tokens will not work if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid token" -ErrorAction Stop } #Header $tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/') #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0 while ($tokenheader.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenheader += "=" } #Payload $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0 while ($tokenPayload.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenPayload += "=" } #Convert to Byte array $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) #Convert to string array $tokenArray = [System.Text.Encoding]::UTF8.GetString($tokenByteArray) #Convert from JSON to PSObject $result = $tokenArray | ConvertFrom-Json return $result } } function Ensure-ConfideAccessToken { <# .SYNOPSIS Ensure a valid access token exists; reconnect if expired or missing and credentials provided. .DESCRIPTION Checks current configuration AccessToken. If missing/expired and Url/ClientId plus either ClientSecret or Certificate supplied, calls Connect-Confide. Throws if token missing and no credentials for first-time connect. Returns the (refreshed) access token string. .PARAMETER Url Base API Url. .PARAMETER ClientId Client Id for authentication. .PARAMETER ClientSecret Client secret (optional credential). .PARAMETER Certificate X509 certificate (optional credential). .PARAMETER IdentityServiceUri Override identity service URI. .OUTPUTS [string] #> [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [string]$Url, [Parameter(Mandatory = $false)] [string]$ClientId, [Parameter(Mandatory = $false)] [string]$ClientSecret, [Parameter(Mandatory = $false)] [Alias('Cert')] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [Parameter(Mandatory = $false)] [string]$IdentityServiceUri ) Process { $config = Get-ConfideConfiguration $accessToken = $config['AccessToken'] $needReconnect = $false if ($accessToken) { try { $payload = Convert-JWTtoken $accessToken if ($payload.exp) { $epochNow = Convert-DateTimeToEpoch -DateTime (Get-Date) if ($payload.exp -le ($epochNow + 60)) { Write-Verbose 'Access token expired or near expiry, will reconnect.'; $needReconnect = $true } } else { Write-Verbose 'Token payload lacks exp; forcing reconnect.'; $needReconnect = $true } } catch { Write-Warning 'Failed to parse existing token; forcing reconnect.'; $needReconnect = $true } } else { Write-Verbose 'No existing token detected.' if ($Url -and $ClientId -and ($ClientSecret -or $Certificate)) { $needReconnect = $true } else { throw 'AccessToken required. Please call Connect-Confide first' } } if ($needReconnect) { if ($Url -and $ClientId -and ($ClientSecret -or $Certificate)) { Write-Verbose 'Reconnecting via Connect-Confide.' if ($PSBoundParameters.ContainsKey('ClientSecret')) { Connect-Confide -Url $Url -ClientId $ClientId -ClientSecret $ClientSecret -IdentityServiceUri $IdentityServiceUri } elseif ($PSBoundParameters.ContainsKey('Certificate')) { Connect-Confide -Url $Url -ClientId $ClientId -Certificate $Certificate -IdentityServiceUri $IdentityServiceUri } else { throw 'Reconnect requested but no credential (ClientSecret/Certificate) supplied.' } } else { throw 'Reconnect required but missing -Url/-ClientId and credential parameters.' } } return (Get-ConfideConfiguration)['AccessToken'] } } |