Private/Get-SpotifyAccessToken.ps1
<#
.SYNOPSIS Gets a Spotify access token .DESCRIPTION Gets a Spotify access token using defined SpotifyApplication It follows the Authorization Code Flow (https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow) .EXAMPLE PS C:\> Get-SpotifyAccessToken -ApplicationName 'dev' Looks for a saved credential named "dev" and tries to get an access token with it's credentials .PARAMETER NoGUI Forces manual Authorization Code retrieval (user needs to copy-paste url in a web browser and copy paste back authorization result) .PARAMETER ApplicationName Specifies the Spotify Application Name (otherwise default is used) #> function Get-SpotifyAccessToken { [CmdletBinding()] param ( [switch] $NoGUI, [String] $ApplicationName ) # Get Application from the Store $Application = Get-SpotifyApplication -Name $ApplicationName # If Token is available if ($Application.Token) { # Check that Access Token is not expired $Expires = [DateTime]::ParseExact($Application.Token.Expires, 'u', $null) if ((Get-Date) -le $Expires.AddSeconds(-10)) { # Access Token is still valid, then use it return $Application.Token.access_token } else { # Access Token is expired, need to be refreshed # ------------------------------ Token Refreshed retrieval ------------------------------ # STEP 1 : Prepare $Uri = 'https://accounts.spotify.com/api/token' $Method = 'Post' $Body = @{ grant_type = 'refresh_token' refresh_token = $Application.Token.refresh_token client_id = $Application.ClientId # alternative way to send the client id and secret client_secret = $Application.ClientSecret # alternative way to send the client id and secret } # STEP 2 : Make request to the Spotify Accounts service try { Write-Verbose 'Send request to refresh access token.' $CurrentTime = Get-Date $Response = Invoke-WebRequest -Uri $Uri -Method $Method -Body $Body } catch { # Don't throw error if Refresh token is revoked or authentication failed if ($_.Exception.Response.StatusCode -ne 400 -and $_.Exception.Response.StatusCode -ne 401) { Throw "Error occured during request of refreshed access token : $([int]$_.Exception.Response.StatusCode) - $($PSItem[0].ToString())" } } # STEP 3 : Parse and save response if ($Response) { $ResponseContent = $Response.Content | ConvertFrom-Json $Token = @{ access_token = $ResponseContent.access_token token_type = $ResponseContent.token_type scope = $ResponseContent.scope expires = $CurrentTime.AddSeconds($ResponseContent.expires_in).ToString('u') refresh_token = if ($ResponseContent.refresh_token) { $ResponseContent.refresh_token } else { $Application.Token.refresh_token } } Set-SpotifyApplication -Name $ApplicationName -Token $Token Write-Verbose 'Successfully saved Refreshed Token' return $Token.access_token } } } # Starting this point, neither valid access token were found nor successful refresh were done # So we start Authorization Code Flow from zero # ------------------------------ Authorization Code retrieval ------------------------------ # STEP 1 : Prepare $EncodedRedirectUri = [System.Web.HTTPUtility]::UrlEncode($Application.RedirectUri) $EncodedScopes = @( # requesting all existing scopes 'ugc-image-upload', 'playlist-modify-public', 'playlist-read-private', 'playlist-modify-private', 'playlist-read-collaborative', 'app-remote-control', 'streaming', 'user-read-playback-position', 'user-read-recently-played', 'user-top-read', 'user-follow-modify', 'user-follow-read', 'user-read-playback-state', 'user-read-currently-playing', 'user-modify-playback-state', 'user-library-read', 'user-library-modify', 'user-read-private', 'user-read-email' ) -join '%20' $State = (New-Guid).ToString() $Uri = 'https://accounts.spotify.com/authorize' $Uri += "?client_id=$($Application.ClientId)" $Uri += '&response_type=code' $Uri += "&redirect_uri=$EncodedRedirectUri" $Uri += "&state=$State" $Uri += "&scope=$EncodedScopes" # if running in a Docker container if ($env:POWERSHELL_DISTRIBUTION_CHANNEL.StartsWith('PSDocker')){ # then Enable NoGUI $NoGUI = $true Write-Verbose 'Running into a Docker Container : switch to manual authorization' } if ($NoGUI) { # if no GUI so no HTTP server $HttpServerReady = $false } else { # Create an Http Server $Listener = [System.Net.HttpListener]::new() $Prefix = $Application.RedirectUri.Substring(0, $Application.RedirectUri.LastIndexOf('/') + 1) # keep uri until the last '/' included $Listener.Prefixes.Add($Prefix) $Listener.Start() if ($Listener.IsListening) { Write-Verbose 'HTTP Server is ready to receive Authorization Code' $HttpServerReady = $true } else { Write-Verbose 'HTTP Server is not ready. Fall back to manual method' $HttpServerReady = $false } } # STEP 2 : Open browser to get Authorization if ($NoGUI) { # if no GUI, ask the user to open the Uri elsewhere Write-Host 'Please, copy-paste the following URL into your web browser : ' Write-Host $Uri -ForegroundColor Yellow } else { if ($IsMacOS) { Write-Host 'Opening Mac OS browser' open $Uri } elseif ($IsLinux) { Write-Host 'Opening Linux browser' Write-Host 'You should have a freedesktop.org-compliant desktop' Start-Process xdg-open $Uri } else { # So we are on Windows Write-Host 'Opening Windows browser' rundll32 url.dll, FileProtocolHandler $Uri } } # STEP 3 : Get response if ($httpServerReady) { Write-Host 'Waiting 30sec for authorization acceptance' $Task = $null $StartTime = Get-Date while ($Listener.IsListening -and ((Get-Date) - $StartTime) -lt '0.00:00:30' ) { if ($null -eq $Task) { $task = $Listener.GetContextAsync() } if ($Task.IsCompleted) { $Context = $task.Result $Task = $null $Response = $context.Request.Url $ContextResponse = $context.Response [string]$html = '<script>close()</script>Thanks! You can close this window now.' $htmlBuffer = [System.Text.Encoding]::UTF8.GetBytes($html) # convert html to bytes $ContextResponse.ContentLength64 = $htmlBuffer.Length $ContextResponse.OutputStream.Write($htmlBuffer, 0, $htmlBuffer.Length) $ContextResponse.OutputStream.Close() break; } } $Listener.Stop() } else { $Response = Read-Host 'Paste here the entire URL that it redirects you to' $Response = [System.Uri]$Response } # STEP 4 : Check and Parse response # check Response if ($Response.OriginalString -eq '') { Throw 'Response of Authorization Code retrieval can''t be empty' } # parse query $ResponseQuery = [System.Web.HttpUtility]::ParseQueryString($Response.Query) # check state if ($ResponseQuery['state'] -ne $State) { Throw 'State returned during Authorization Code retrieval doesn''t match state passed' } # check if an error has been returned if ($ResponseQuery['error']) { Throw "Error occured during Authorization Code retrieval : $($ResponseQuery['error'])" } # all checks are passed, we should have the code if ($ResponseQuery['code']) { $AuthorizationCode = $ResponseQuery['code'] } else { Throw 'Authorization Code not returned during Authorization Code retrieval' } # Authorization Code is in $AuthorizationCode # ------------------------------ Token retrieval ------------------------------ # STEP 1 : Prepare $Uri = 'https://accounts.spotify.com/api/token' $Method = 'Post' $Body = @{ grant_type = 'authorization_code' code = $AuthorizationCode redirect_uri = $Application.RedirectUri client_id = $Application.ClientId # alternative way to send the client id and secret client_secret = $Application.ClientSecret # alternative way to send the client id and secret } # STEP 2 : Make request to the Spotify Accounts service try { Write-Verbose 'Send request to get access token.' $CurrentTime = Get-Date $Response = Invoke-WebRequest -Uri $Uri -Method $Method -Body $Body } catch { Throw "Error occured during request of access token : $($PSItem[0].ToString())" } # STEP 3 : Parse and save response $ResponseContent = $Response.Content | ConvertFrom-Json $Token = @{ access_token = $ResponseContent.access_token token_type = $ResponseContent.token_type scope = $ResponseContent.scope expires = $CurrentTime.AddSeconds($ResponseContent.expires_in).ToString('u') refresh_token = $ResponseContent.refresh_token } Set-SpotifyApplication -Name $ApplicationName -Token $Token Write-Verbose 'Successfully saved Token' return $Token.access_token } |