Skip to content

Commit

Permalink
Update to Authentication to seperate oAuth to seperate module.
Browse files Browse the repository at this point in the history
Automate reconnect after expired token
custom PSSpotify types and views
Increased piping support
Pester and Gherkin tests. Currently about 30% code coverage
  • Loading branch information
Ryan Bartram committed Aug 21, 2017
1 parent 131cbe0 commit 00ffeb6
Show file tree
Hide file tree
Showing 28 changed files with 3,135 additions and 408 deletions.
119 changes: 13 additions & 106 deletions .vscode/tasks.json
@@ -1,113 +1,20 @@
{
"version": "0.1.0",
"command": "${env.windir}\\sysnative\\windowspowershell\\v1.0\\PowerShell.exe",
"isShellCommand": true,
"showOutput": "always",
"version": "2.0.0",
"command": "c:\\windows\\system32\\windowspowershell\\v1.0\\PowerShell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass"
],
"tasks": [
{
"taskName": "Continuous Pester: Modules",
"isTestCommand": true,
"suppressTaskName": true,
"args": [
"Write-Host 'Starting continuous testing';",
"Import-Module -Name Pester -RequiredVersion 3.4.3 -Force;",
"Import-Module -Name PowerShellGuard -Force;",
"New-Guard -Path '${workspaceRoot}' -PathFilter '*.psm1' -Recurse -TestCommand {write-host \"Invoking Continuous Pester: Modules\"; Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}} -TestPath '${workspaceRoot}\\Tests';",
"New-Guard -Path '${workspaceRoot}\\Tests' -PathFilter '*.tests.ps1' -Recurse -TestCommand {write-host \"Invoking Continuous Pester: Modules\"; Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}} -TestPath '${workspaceRoot}\\Tests';",
"Wait-Guard"
],
"isBackground": true,
"problemMatcher": [
{
"owner": "Continuous Pester: Modules",
"fileLocation": "absolute",
"pattern": [
{
"regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$",
"message": 1
},
{
"regexp": "^\\s+at\\s+[^,]+,\\s*(.*?):\\s+line\\s+(\\d+)$",
"file": 1,
"line": 2
}
],
"watching": {
"activeOnStart": true,
"beginsPattern": "^Invoking Continuous Pester: Modules$",
"endsPattern": "^Passed:\\s\\d+\\sFailed:\\s\\d+\\sSkipped:\\s\\d+\\sPending:\\s\\d+\\sInconclusive:\\s\\d+\\s$"
}
}
]
},
{
"taskName": "Continuous Pester: Scripts",
"isTestCommand": true,
"suppressTaskName": true,
"args": [
"Write-Host 'Starting continuous testing';",
"Import-Module -Name Pester -RequiredVersion 3.4.3 -Force;",
"Import-Module -Name PowerShellGuard -Force;",
"New-Guard -Path '${workspaceRoot}' -PathFilter '*.ps1' -Recurse -TestCommand {write-host \"Invoking Continuous Pester: Scripts\"; Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}} -TestPath '${workspaceRoot}\\Tests';",
"New-Guard -Path '${workspaceRoot}\\Tests' -PathFilter '*.tests.ps1' -Recurse -TestCommand {write-host \"Invoking Continuous Pester: Scripts\"; Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}} -TestPath '${workspaceRoot}\\Tests';",
"Wait-Guard"
],
"isBackground": true,
"problemMatcher": [
{
"owner": "Continuous Pester: Scripts",
"fileLocation": "absolute",
"pattern": [
{
"regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$",
"message": 1
},
{
"regexp": "^\\s+at\\s+[^,]+,\\s*(.*?):\\s+line\\s+(\\d+)$",
"file": 1,
"line": 2
}
],
"watching": {
"activeOnStart": true,
"beginsPattern": "^Invoking Continuous Pester: Scripts$",
"endsPattern": "^Passed:\\s\\d+\\sFailed:\\s\\d+\\sSkipped:\\s\\d+\\sPending:\\s\\d+\\sInconclusive:\\s\\d+\\s$"
}
}
]
},
{
"taskName": "Package Module and Increment",
"isBuildCommand": false,
"suppressTaskName": true,
"args": [
"Write-Host 'Packaging ${workspaceRootFolderName}';",
"$Module = Import-Module '${workspaceRoot}' -Force -PassThru;",
"$Module.Version -match '(.?)(\\d+)$'| out-null; $NewVersion = ($Module.Version -replace '.?\\d+$' ,\"$($matches[1]+([int32]$matches[2] + 1))\");",
"Update-ModuleManifest -ModuleVersion $NewVersion -Path '${workspaceRoot}\\${workspaceRootFolderName}.psd1' -ea SilentlyContinue;",
"$Path = split-path '${workspaceRoot}' -parent; $Path += '\\{0}_{1}.zip';",
"gci '${workspaceRoot}' -Exclude docs, .vscode | Compress-Archive -U -DestinationPath ($path -f $Module.Name, $NewVersion);",
"write-host \"Module packaged to $($path -f $Module.Name, $NewVersion)\""
],
"isBackground": true
},
{
"taskName": "Package Module",
"isBuildCommand": true,
"suppressTaskName": true,
"args": [
"Write-Host 'Packaging ${workspaceRootFolderName}';",
"$Module = Import-Module '${workspaceRoot}' -Force -PassThru;",
"$Path = split-path '${workspaceRoot}' -parent; $Path += '\\{0}_{1}.zip';",
"gci '${workspaceRoot}' -Exclude docs, .vscode | Compress-Archive -U -DestinationPath ($path -f $Module.Name, $Module.Version);",
"write-host \"Module packaged to $($path -f $Module.Name, $Module.Version)\""
],
"isBackground": true
}
]
"suppressTaskName": true,
"tasks": [{
"taskName": "Test",
"suppressTaskName": true,
"isTestCommand": true,
"showOutput": "always",
"args": [
"Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}"
],
"problemMatcher": "$pester"
}]
}
2 changes: 2 additions & 0 deletions Localized/en-US/Strings.psd1
@@ -1,2 +1,4 @@
ConvertFrom-StringData @'
SessionNotFound = "Spotify Session not established. Please run Connect-Spotify."
TokenExpiredGenerating = "Session has expired. Requesting new token."
'@
211 changes: 62 additions & 149 deletions Modules/Auth.psm1
@@ -1,5 +1,10 @@
$currentPath = Split-Path -Parent $MyInvocation.MyCommand.Path

$_AuthorizationEndpoint = "https://accounts.spotify.com/authorize"
$_TokenEndpoint = "https://accounts.spotify.com/api/token"
$_RootAPIEndpoint = "https://api.spotify.com/v1"
$_RedirectUri = "https://localhost:8001"

Import-LocalizedData -BindingVariable Strings -BaseDirectory $currentPath\..\Localized -FileName Strings.psd1 -UICulture en-US

$ValidPermissions = @(
Expand All @@ -13,7 +18,11 @@ $ValidPermissions = @(
'playlist-modify-private',
'playlist-read-private',
'playlist-read-collaborative',
'user-read-birthdate'
'user-read-birthdate',
'user-follow-read',
'user-follow-modify',
"user-library-modify",
"user-library-read"
)

function Connect-Spotify {
Expand All @@ -26,7 +35,23 @@ function Connect-Spotify {

[parameter()]
[string]
$RedirectUri = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData["RedirectUri"],
$RootAPIEndpoint = $_RootAPIEndpoint,

[parameter()]
[string]
$AuthorizationEndpoint = $_AuthorizationEndpoint,

[parameter()]
[string]
$TokenEndpoint = $_TokenEndpoint,

[parameter()]
[string]
$RedirectUri = $_RedirectUri,

[parameter()]
[string]
$RefreshToken,

[parameter()]
[switch]
Expand All @@ -51,22 +76,28 @@ function Connect-Spotify {
}

process {
$AuthCode = Get-AuthorizationCode -ClientIdSecret $ClientIdSecret -Permissions $PSBoundParameters["Permissions"] -RedirectUri $redirectUri

$AccessToken = Get-AccessToken -ClientIdSecret $ClientIdSecret -AuthorizationCode $AuthCode -RedirectUri $RedirectUri
if ($RefreshToken -eq "") {
$AuthCode = Get-AuthorizationCode -AuthorizationEndpoint $AuthorizationEndpoint -ClientIdSecret $ClientIdSecret -Permissions $PSBoundParameters["Permissions"] -RedirectUri $RedirectUri

$AccessToken = Get-AccessToken -TokenEndpoint $TokenEndpoint -ClientIdSecret $ClientIdSecret -AuthorizationCode $AuthCode -RedirectUri $RedirectUri
$RefreshToken = $AccessToken.refresh_token
}
else {
$AccessToken = Get-AccessToken -TokenEndpoint $TokenEndpoint -ClientIdSecret $ClientIdSecret -RefreshToken $RefreshToken
}

$Session = [PSCustomObject]@{
Headers = @{Authorization = "$($AccessToken.token_type) $($AccessToken.access_token)"; "content-type" = "application/json"}
RootUrl = "https://api.spotify.com/v1"
$Session = New-Object PSSpotify.SessionInfo -property @{
Headers = @{Authorization = "$($AccessToken.token_type) $($AccessToken.access_token)"; "contenttype" = "application/json"}
RootUrl = $RootAPIEndpoint
Expires = $AccessToken.expires_in
RefreshToken = $AccessToken.refresh_token
RefreshToken = $RefreshToken
APIEndpoints = @{AuthorizationEndpoint = $AuthorizationEndpoint; TokenEndpoint = $TokenEndpoint; RedirectUri = $RedirectUri}
}

$Profile = Get-SpotifyProfile -Session $Session

$Session | Add-Member -MemberType NoteProperty -Name CurrentUser -Value $Profile

$Session.PSObject.TypeNames.Insert(0, "PSSpotify.SessionInfo")
$Session.CurrentUser = $Profile

$Global:SpotifySession = $Session

if ($KeepCredential) {
Expand All @@ -77,156 +108,38 @@ function Connect-Spotify {
}
}

function Get-AuthorizationCode {
[cmdletbinding()]
param(
[parameter(Mandatory)]
[pscredential]
$ClientIdSecret,

[parameter()]
[string]
$RedirectUri = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData["RedirectUri"]
)

DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

$ParameterName = 'Permissions'
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $false
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ValidPermissions)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string[]], $AttributeCollection)
$PSBoundParameters["Permissions"] = "-"
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)

return $RuntimeParameterDictionary
}

begin {
$redirectUriEncoded = [System.Web.HttpUtility]::UrlEncode($redirectUri)
}

process {
[void]$(
$url = [string]::Format("https://accounts.spotify.com/authorize?response_type=code&redirect_uri={0}&client_id={1}&scope={2}", `
$redirectUriEncoded, `
$ClientIdSecret.UserName, `
($PSBoundParameters["Permissions"] -join '%20'))

Add-Type -AssemblyName System.Windows.Forms
$form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width = 440; Height = 640}
$web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width = 420; Height = 600; Url = $Url }

$DocComp = {
$ReturnUrl = $web.Url.AbsoluteUri
if ($ReturnUrl -match "error=[^&]*|code=[^&]*") {
$form.Close()
}
}

$web.ScriptErrorsSuppressed = $true
$web.Add_DocumentCompleted($DocComp)
$form.Controls.Add($web)
$form.Add_Shown( {$form.Activate()})
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterParent
$form.ShowDialog()


$queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query)

$output = @{}

foreach ($key in $queryOutput.Keys) {
$output["$key"] = $queryOutput[$key]
}

$PSCmdlet.WriteObject($output["Code"])
)
}
}

function Get-AccessToken {
[cmdletbinding()]
param(
[parameter(Mandatory)]
[pscredential]
$ClientIdSecret,

[parameter(Mandatory)]
[string]
$AuthorizationCode,

[parameter()]
[string]
$RedirectUri = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData["RedirectUri"]
)

begin {
$redirectUriEncoded = [System.Web.HttpUtility]::UrlEncode($redirectUri)
}

process {
[void]$(
$body = [string]::format("grant_type=authorization_code&redirect_uri={0}&client_id={1}&client_secret={2}&code={3}", `
$redirectUriEncoded, `
$ClientIdSecret.UserName, `
[System.Web.HttpUtility]::UrlEncode($ClientIdSecret.GetNetworkCredential().Password), `
$AuthorizationCode
)

$Authorization = Invoke-RestMethod https://accounts.spotify.com/api/token `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body $body `
-ErrorAction STOP

$PSCmdlet.WriteObject($Authorization)
)
}
}

function Assert-AuthToken {
[cmdletbinding()]
param(
[parameter()]
[parameter(Mandatory)]
$Session = $Global:SpotifySession
)

process {
try {
Get-SpotifyProfile -Session $Session
} catch {
$Url = "$($Session.RootUrl)/me"

Invoke-RestMethod -Headers $Session.Headers `
-Uri $Url `
-Method Get | out-null
}
catch {
$Error = ConvertFrom-Json $_.ErrorDetails.Message
if ($Error.Error.Message -eq "The access token expired") {
[void]$(
$ClientIdSecret = $Global:SpotifyCredential

if ($ClientIdSecret -eq $null) {
$ClientIdSecret = get-credential
If ($Strings -eq $null) {
$pscmdlet.WriteVerbose("Session has expired. Requesting new token.")
}

$body = [string]::format("grant_type=refresh_token&client_id={1}&client_secret={2}&refresh_token={0}", `
$Global:SpotifySession.RefreshToken, `
$ClientIdSecret.UserName, `
[System.Web.HttpUtility]::UrlEncode($ClientIdSecret.GetNetworkCredential().Password)
)

$Authorization = Invoke-RestMethod https://accounts.spotify.com/api/token `
-Method Post -ContentType "application/x-www-form-urlencoded" `
-Body $body `
-ErrorAction STOP

$Session = [PSCustomObject]@{
Headers = @{Authorization = "$($Authorization.token_type) $($Authorization.access_token)"}
RootUrl = "https://api.spotify.com/v1"
Expires = $Authorization.expires_in
RefreshToken = $Global:SpotifySession.RefreshToken
else {
$pscmdlet.WriteVerbose($Strings["TokenExpiredGenerating"])
}
$Session.PSObject.TypeNames.Insert(0, "PSSpotify.SessionInfo")
$Global:SpotifySession = $Session

Connect-Spotify -ClientIdSecret $Global:SpotifyCredential `
-RootAPIEndpoint $Session.RootUrl `
-RefreshToken $Session.RefreshToken `
-AuthorizationEndpoint $Session.APIEndpoints.AuthorizationEndpoint `
-TokenEndpoint $Session.APIEndpoints.TokenEndpoint `
-RedirectUri $Session.APIEndpoints.RedirectUri
)
}
}
Expand Down

0 comments on commit 00ffeb6

Please sign in to comment.