diff --git a/.dev/localImport.ps1 b/.dev/localImport.ps1 new file mode 100644 index 000000000..cbc719808 --- /dev/null +++ b/.dev/localImport.ps1 @@ -0,0 +1,5 @@ +$repoPath = (Split-Path $PSScriptRoot -Parent) +$modulePath = Join-Path $repoPath 'src' 'GitHub' + +$PSModulePath += ";$modulePath" +Import-Module "$modulePath" diff --git a/scripts/GitHubAPI.ps1 b/scripts/GitHubAPI.ps1 index d8482a6a4..3e35bde85 100644 --- a/scripts/GitHubAPI.ps1 +++ b/scripts/GitHubAPI.ps1 @@ -1,4 +1,4 @@ $APIDocURI = 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json' -$Response = Invoke-WebRequest -Uri $APIDocURI -Method Get -UseBasicParsing +$Response = Invoke-RestMethod -Uri $APIDocURI -Method Get $APIDoc = $Response.Content | ConvertFrom-Json $APIDoc.tags diff --git a/src/GitHub/private/Auth/DeviceFlow/Invoke-GitHubDeviceCodeLogin.ps1 b/src/GitHub/private/Auth/DeviceFlow/Invoke-GitHubDeviceCodeLogin.ps1 new file mode 100644 index 000000000..41b68b17e --- /dev/null +++ b/src/GitHub/private/Auth/DeviceFlow/Invoke-GitHubDeviceCodeLogin.ps1 @@ -0,0 +1,41 @@ +function Invoke-GitHubDeviceCodeLogin { + <# + .SYNOPSIS + Starts the GitHub Device Flow login process. + + .DESCRIPTION + Starts the GitHub Device Flow login process. This will prompt the user to visit a URL and enter a code. + + .EXAMPLE + Invoke-GitHubDeviceCodeLogin + + This will start the GitHub Device Flow login process. + The user gets prompted to visit a URL and enter a code. + + .NOTES + For more info about the Device Flow visit: + https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-cli-with-a-github-app + #> + [OutputType([void])] + [CmdletBinding()] + param( + # The Client ID of the GitHub App. + [Parameter()] + [string] $ClientID = 'Iv1.f26b61bc99e69405' + ) + + $deviceCodeResponse = Request-GitHubDeviceCode -ClientID $ClientID + + $deviceCode = $deviceCodeResponse.device_code + $interval = $deviceCodeResponse.interval + $userCode = $deviceCodeResponse.user_code + $verificationUri = $deviceCodeResponse.verification_uri + + Write-Host "Please visit: $verificationUri" + Write-Host "and enter code: $userCode" + + $token = Wait-GitHubToken -DeviceCode $deviceCode -ClientID $ClientID -Interval $interval + + Write-Host 'Successfully authenticated!' + $token +} diff --git a/src/GitHub/private/Auth/DeviceFlow/Request-GitHubDeviceCode.ps1 b/src/GitHub/private/Auth/DeviceFlow/Request-GitHubDeviceCode.ps1 new file mode 100644 index 000000000..9c10d9431 --- /dev/null +++ b/src/GitHub/private/Auth/DeviceFlow/Request-GitHubDeviceCode.ps1 @@ -0,0 +1,41 @@ +function Request-GitHubDeviceCode { + <# + .SYNOPSIS + Request a GitHub Device Code. + + .DESCRIPTION + Request a GitHub Device Code. + + .EXAMPLE + Request-GitHubDeviceCode -ClientID $ClientID + + This will request a GitHub Device Code. + + .NOTES + For more info about the Device Flow visit: + https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-cli-with-a-github-app + #> + [OutputType([PSCustomObject])] + [CmdletBinding()] + param( + # The Client ID of the GitHub App. + [Parameter()] + [string] $ClientID + ) + $RESTParams = @{ + Uri = 'https://github.com/login/device/code' + Method = 'POST' + Body = @{ 'client_id' = $ClientID } + Headers = @{ 'Accept' = 'application/json' } + } + try { + Write-Verbose ($RESTParams.GetEnumerator() | Out-String) + + $deviceCodeResponse = Invoke-RestMethod @RESTParams -Verbose:$false + return $deviceCodeResponse + } catch { + Write-Error $_.Exception.Message + throw $_ + } +} + diff --git a/src/GitHub/private/Auth/DeviceFlow/Request-GitHubToken.ps1 b/src/GitHub/private/Auth/DeviceFlow/Request-GitHubToken.ps1 new file mode 100644 index 000000000..e9bced488 --- /dev/null +++ b/src/GitHub/private/Auth/DeviceFlow/Request-GitHubToken.ps1 @@ -0,0 +1,55 @@ +function Request-GitHubToken { + <# + .SYNOPSIS + Request a GitHub token using the Device Flow. + + .DESCRIPTION + Request a GitHub token using the Device Flow. + This will poll the GitHub API until the user has entered the code. + + .EXAMPLE + Request-GitHubToken -DeviceCode $deviceCode -ClientID $ClientID + + This will poll the GitHub API until the user has entered the code. + + .NOTES + For more info about the Device Flow visit: + https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-cli-with-a-github-app + #> + [OutputType([PSCustomObject])] + [CmdletBinding()] + param( + # The `device_code` used to request the access token. + [Parameter(Mandatory)] + [string] $DeviceCode, + + # The Client ID of the GitHub App. + [Parameter()] + [string] $ClientID, + + # The refresh token used create a new access token. + [Parameter()] + [string] $RefreshToken + ) + + $RESTParams = @{ + Uri = 'https://github.com/login/oauth/access_token' + Method = 'POST' + Body = @{ + 'client_id' = $ClientID + 'device_code' = $DeviceCode + 'grant_type' = 'urn:ietf:params:oauth:grant-type:device_code' + } + Headers = @{ 'Accept' = 'application/json' } + } + + try { + Write-Verbose ($RESTParams.GetEnumerator() | Out-String) + + $tokenResponse = Invoke-RestMethod @RESTParams -Verbose:$false + return $tokenResponse + } catch { + Write-Error $_.Exception.Message + throw $_ + } +} diff --git a/src/GitHub/private/Auth/DeviceFlow/Wait-GitHubToken.ps1 b/src/GitHub/private/Auth/DeviceFlow/Wait-GitHubToken.ps1 new file mode 100644 index 000000000..3e3c8a66d --- /dev/null +++ b/src/GitHub/private/Auth/DeviceFlow/Wait-GitHubToken.ps1 @@ -0,0 +1,76 @@ + +function Wait-GitHubToken { + <# + .SYNOPSIS + Waits for the GitHub Device Flow to complete. + + .DESCRIPTION + Waits for the GitHub Device Flow to complete. + This will poll the GitHub API until the user has entered the code. + + .EXAMPLE + Wait-GitHubToken -DeviceCode $deviceCode -ClientID $ClientID -Interval $interval + + This will poll the GitHub API until the user has entered the code. + + .NOTES + For more info about the Device Flow visit: + https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-cli-with-a-github-app + #> + [OutputType([PSCustomObject])] + [CmdletBinding()] + param( + # The `device_code` used to request the token. + [Parameter(Mandatory)] + [string] $DeviceCode, + + # The Client ID of the GitHub App. + [Parameter()] + [string] $ClientID, + + # The interval to wait between polling for the token. + [Parameter()] + [int] $Interval = 5 + ) + + do { + $response = Request-GitHubToken -DeviceCode $DeviceCode -ClientID $ClientID + if ($response.error) { + switch ($response.error) { + 'authorization_pending' { + # The user has not yet entered the code. + # Wait, then poll again. + Write-Verbose $response.error_description + Start-Sleep -Seconds $interval + continue + } + 'slow_down' { + # The app polled too fast. + # Wait for the interval plus 5 seconds, then poll again. + Write-Verbose $response.error_description + Start-Sleep -Seconds ($interval + 5) + continue + } + 'expired_token' { + # The `device_code` expired, and the process needs to restart. + Write-Error $response.error_description + exit 1 + } + 'access_denied' { + # The user cancelled the process. Stop polling. + Write-Error $response.error_description + exit 1 + } + default { + # The response contains an access token. Stop polling. + Write-Error 'Unknown error:' + Write-Error $response.error + Write-Error $response.error_description + Write-Error $response.error_uri + break + } + } + } + } until ($response.access_token) + $response +} diff --git a/src/GitHub/public/Core/Connect-GitHubAccount.ps1 b/src/GitHub/public/Core/Connect-GitHubAccount.ps1 index 2c232980f..b858207f4 100644 --- a/src/GitHub/public/Core/Connect-GitHubAccount.ps1 +++ b/src/GitHub/public/Core/Connect-GitHubAccount.ps1 @@ -11,7 +11,7 @@ [Parameter()] [String] $Repo, - [Parameter(Mandatory)] + [Parameter()] [String] $Token, [Parameter()] @@ -21,6 +21,12 @@ [string] $Version = '2022-11-28' ) + if ($Token) { + $script:Token = $Token + } else { + $script:Token = Invoke-GitHubDeviceCodeLogin + } + $script:APIBaseURI = $APIBaseURI $script:Owner = $Owner $script:Repo = $Repo