Private/Invoke-AuthorizationPKCEFlow.ps1
class SpotifyToken { [string] $token [string[]] $scopes [datetime] $expiration } # reference: # https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow function Invoke-AuthorizationPKCEFlow { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string[]] $Scopes, [Parameter(Mandatory=$false)] [string] $ClientId, [Parameter(Mandatory=$false)] [string] $RedirectURI, [ValidateScript({Test-Path $_})] [Parameter(Mandatory=$false)] [string] $ConfigFile ) # ensure we have required configuration value(s) $cId = Get-ClientId -Params $PSBoundParameters $rURI = Get-RedirectURI -Params $PSBoundParameters # generate code verifier $possible = [string[]][char[]]( 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ) $verifier = [String]::Join( "", (Get-Random -Count 128 -InputObject $possible) ) $bytes = [System.Text.Encoding]::UTF8.GetBytes($verifier) $hasher = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $hasher.ComputeHash($bytes) # https://stackoverflow.com/questions/63482575/ $b64hash = [System.Convert]::ToBase64String($hashBytes). Replace('=', ''). Replace('+', '-'). Replace('/', '_') # initiate flow $urlParams = @{ client_id = $cId response_type = 'code' redirect_uri = $rURI scope = [string]::Join(" ", $scopes) code_challenge_method = 'S256' code_challenge = $b64hash } $uParams = foreach ($param in $urlParams.Keys) { [string]::Format( "{0}={1}", $param, $urlParams.$param ) } $uri = [string]::Format( "{0}?{1}", $script:AUTH_URI, [string]::Join("&", $uParams) ) $params = @{ URI = $uri MaximumRedirection = 0 ErrorAction = 'SilentlyContinue' } if ($PSVersionTable.PSVersion.Major -gt 5) { $params.Add('SkipHttpErrorCheck', $true) } $response = Invoke-WebRequest @params $authPage = $response.Headers.Location[0] # prompt user to authenticate Start-Process $authPage Write-Information 'Opening authentication page in your web browser...' # start our listener to catch redirected code $timeout = 30 $sb = { $srv = [System.Net.HttpListener]::New() if ($rURI[-1] -ne '/') { $rURI += '/' } try { $srv.Prefixes.Add($rURI) $srv.Start() $context = $srv.GetContext() $query = $context.Request.QueryString $response = $context.Response $response.StatusCode = 200 $response.ContentType = 'text/html' $data = ` '<html> <head><script>window.close();</script></head> <body>Hello! Goodbye!</body> </html>' $buffer = [System.Text.Encoding]::UTF8.GetBytes($data) $response.ContentLength64 = $buffer.Length $response.OutputStream.Write($buffer, 0, $buffer.Length) $response.Close() $code = $query.Get('code') } finally { $srv.Close() } Write-Output $code } $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = (Get-Process -Id $PID).Path $pinfo.RedirectStandardOutput = $true $pinfo.UseShellExecute = $false $pinfo.Arguments = @( "-NonInteractive", "-Command", [string]::Format( "{0}`n{1}", "`$rURI = '$rURI'", $sb.ToString() ) ) $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo $p.Start() | Out-Null $counter = 0 while ($counter -lt $timeout) { if ($p.HasExited) { break } Start-Sleep -Seconds 1 } if (! $p.HasExited) { Stop-Process -Force -Id $p.Id throw "Authentication timed out after $timeout seconds" } $code = $p.StandardOutput.ReadToEnd().Trim() Write-Debug "Received code: $code" Write-Information "Authentication complete" # get access token $params = @{ URI = $script:TOKEN_URI Method = 'Post' ContentType = 'application/x-www-form-urlencoded' Body = @{ grant_type = 'authorization_code' code = $code redirect_uri = $rURI client_id = $cId code_verifier = $verifier } } try { $tokenResponse = (Invoke-WebRequest @params).Content | ConvertFrom-Json } catch { throw 'Failed to acquire access token' } $script:TOKENS.Add([SpotifyToken]@{ token = $tokenResponse.access_token scopes = $Scopes expiration = ([datetime]::Now).AddSeconds($tokenResponse.expires_in) }) | Out-Null return $tokenResponse.access_token } |