Skip to content

Commit 3d8c319

Browse files
authored
Merge pull request #478 from mjcheetham/gh-device-ui
Add explicit GitHub device code authentication option
2 parents 613d894 + 0fb32aa commit 3d8c319

28 files changed

+618
-68
lines changed

docs/configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ If this option is not set, then the available authentication modes will be autom
185185
Value|Authentication Mode
186186
-|-
187187
_(unset)_|Automatically detect modes
188-
`oauth`|OAuth-based authentication
188+
`oauth`|Expands to: `browser, device`
189+
`browser`|OAuth authentication via a web browser _(requires a GUI)_
190+
`device`|OAuth authentication with a device code
189191
`basic`|Basic/PAT-based authentication
190192

191193
#### Example

docs/environment.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,9 @@ If this option is not set, then the available authentication modes will be autom
324324
Value|Authentication Mode
325325
-|-
326326
_(unset)_|Automatically detect modes
327-
`oauth`|OAuth-based authentication
327+
`oauth`|Expands to: `browser, device`
328+
`browser`|OAuth authentication via a web browser _(requires a GUI)_
329+
`device`|OAuth authentication with a device code
328330
`basic`|Basic/PAT-based authentication
329331

330332
##### Windows

src/shared/GitHub.Tests/GitHubHostProviderTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void GitHubHostProvider_GetCredentialServiceUrl(string protocol, string h
9292

9393

9494
[Theory]
95-
[InlineData("https://example.com", "oauth", AuthenticationModes.OAuth)]
95+
[InlineData("https://example.com", "browser", AuthenticationModes.Browser)]
9696
[InlineData("https://github.com", "NOT-A-REAL-VALUE", GitHubConstants.DotComAuthenticationModes)]
9797
[InlineData("https://GitHub.Com", "NOT-A-REAL-VALUE", GitHubConstants.DotComAuthenticationModes)]
9898
[InlineData("https://github.com", "none", GitHubConstants.DotComAuthenticationModes)]
@@ -170,7 +170,7 @@ public async Task GitHubHostProvider_GenerateCredentialAsync_UnencryptedHttp_Thr
170170
}
171171

172172
[Fact]
173-
public async Task GitHubHostProvider_GenerateCredentialAsync_OAuth_ReturnsCredential()
173+
public async Task GitHubHostProvider_GenerateCredentialAsync_Browser_ReturnsCredential()
174174
{
175175
var input = new InputArguments(new Dictionary<string, string>
176176
{
@@ -194,9 +194,9 @@ public async Task GitHubHostProvider_GenerateCredentialAsync_OAuth_ReturnsCreden
194194

195195
var ghAuthMock = new Mock<IGitHubAuthentication>(MockBehavior.Strict);
196196
ghAuthMock.Setup(x => x.GetAuthenticationAsync(expectedTargetUri, null, It.IsAny<AuthenticationModes>()))
197-
.ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.OAuth));
197+
.ReturnsAsync(new AuthenticationPromptResult(AuthenticationModes.Browser));
198198

199-
ghAuthMock.Setup(x => x.GetOAuthTokenAsync(expectedTargetUri, It.IsAny<IEnumerable<string>>()))
199+
ghAuthMock.Setup(x => x.GetOAuthTokenViaBrowserAsync(expectedTargetUri, It.IsAny<IEnumerable<string>>()))
200200
.ReturnsAsync(response);
201201

202202
var ghApiMock = new Mock<IGitHubRestApi>(MockBehavior.Strict);
@@ -212,7 +212,7 @@ public async Task GitHubHostProvider_GenerateCredentialAsync_OAuth_ReturnsCreden
212212
Assert.Equal(tokenValue, credential.Password);
213213

214214
ghAuthMock.Verify(
215-
x => x.GetOAuthTokenAsync(
215+
x => x.GetOAuthTokenViaBrowserAsync(
216216
expectedTargetUri, expectedOAuthScopes),
217217
Times.Once);
218218
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using GitHub.UI.ViewModels;
4+
using GitHub.UI.Views;
5+
using Microsoft.Git.CredentialManager;
6+
using Microsoft.Git.CredentialManager.UI;
7+
8+
namespace GitHub.UI.Commands
9+
{
10+
public class DeviceCodeCommandImpl : DeviceCodeCommand
11+
{
12+
public DeviceCodeCommandImpl(ICommandContext context) : base(context) { }
13+
14+
protected override Task ShowAsync(DeviceCodeViewModel viewModel, CancellationToken ct)
15+
{
16+
return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentHandle(), ct);
17+
}
18+
}
19+
}

src/shared/GitHub.UI.Avalonia/Controls/TesterWindow.axaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,17 @@
6060
<Button Classes="accent" Content="Show" Click="ShowTwoFactorCode" />
6161
</StackPanel>
6262
</TabItem>
63+
64+
<TabItem Header="Device Code">
65+
<StackPanel>
66+
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
67+
<Label Grid.Row="0" Grid.Column="0" Content="User Code" />
68+
<TextBox Grid.Row="0" Grid.Column="1" x:Name="userCode" />
69+
<Label Grid.Row="1" Grid.Column="0" Content="Verification Url" />
70+
<TextBox Grid.Row="1" Grid.Column="1" x:Name="verificationUrl" />
71+
</Grid>
72+
<Button Classes="accent" Content="Show" Click="ShowDeviceCode" />
73+
</StackPanel>
74+
</TabItem>
6375
</TabControl>
6476
</Window>

src/shared/GitHub.UI.Avalonia/Controls/TesterWindow.axaml.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ private void ShowCredentials(object sender, RoutedEventArgs e)
5555
var vm = new CredentialsViewModel(_environment)
5656
{
5757
ShowBrowserLogin = this.FindControl<CheckBox>("useBrowser").IsChecked ?? false,
58+
ShowDeviceLogin = this.FindControl<CheckBox>("useDevice").IsChecked ?? false,
5859
ShowTokenLogin = this.FindControl<CheckBox>("usePat").IsChecked ?? false,
5960
ShowBasicLogin = this.FindControl<CheckBox>("useBasic").IsChecked ?? false,
6061
EnterpriseUrl = this.FindControl<TextBox>("enterpriseUrl").Text,
@@ -75,5 +76,17 @@ private void ShowTwoFactorCode(object sender, RoutedEventArgs e)
7576
var window = new DialogWindow(view) {DataContext = vm};
7677
window.ShowDialog(this);
7778
}
79+
80+
private void ShowDeviceCode(object sender, RoutedEventArgs e)
81+
{
82+
var vm = new DeviceCodeViewModel(_environment)
83+
{
84+
UserCode = this.FindControl<TextBox>("userCode").Text,
85+
VerificationUrl = this.FindControl<TextBox>("verificationUrl").Text,
86+
};
87+
var view = new DeviceCodeView();
88+
var window = new DialogWindow(view) {DataContext = vm};
89+
window.ShowDialog(this);
90+
}
7891
}
7992
}

src/shared/GitHub.UI.Avalonia/GitHub.UI.Avalonia.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
<DependentUpon>DeviceCodeView.axaml</DependentUpon>
3939
<SubType>Code</SubType>
4040
</Compile>
41+
<Compile Update="Views\DeviceCodeView.axaml.cs">
42+
<DependentUpon>DeviceCodeView.axaml</DependentUpon>
43+
<SubType>Code</SubType>
44+
</Compile>
4145
</ItemGroup>
4246

4347
</Project>

src/shared/GitHub.UI.Avalonia/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ private static void AppMain(object o)
5050
{
5151
app.RegisterCommand(new CredentialsCommandImpl(context));
5252
app.RegisterCommand(new TwoFactorCommandImpl(context));
53+
app.RegisterCommand(new DeviceCodeCommandImpl(context));
5354

5455
int exitCode = app.RunAsync(args)
5556
.ConfigureAwait(false)

src/shared/GitHub.UI.Avalonia/Views/CredentialsView.axaml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:controls="clr-namespace:GitHub.UI.Controls"
66
xmlns:vm="clr-namespace:GitHub.UI.ViewModels;assembly=GitHub.UI.Shared"
7+
xmlns:converters="clr-namespace:Microsoft.Git.CredentialManager.UI.Converters;assembly=Microsoft.Git.CredentialManager.UI.Avalonia"
78
mc:Ignorable="d" d:DesignWidth="420"
89
x:Class="GitHub.UI.Views.CredentialsView">
910
<Design.DataContext>
@@ -50,10 +51,15 @@
5051
</Style>
5152
</TabControl.Styles>
5253

53-
<TabItem IsVisible="{Binding $self.IsEnabled}"
54-
IsEnabled="{Binding ShowBrowserLogin}">
54+
<TabItem IsVisible="{Binding $self.IsEnabled}">
55+
<TabItem.IsEnabled>
56+
<MultiBinding Converter="{x:Static converters:BoolConvertersEx.Or}">
57+
<Binding Path="ShowBrowserLogin" />
58+
<Binding Path="ShowDeviceLogin" />
59+
</MultiBinding>
60+
</TabItem.IsEnabled>
5561
<TabItem.Header>
56-
<TextBlock Text="Browser" FontSize="12" />
62+
<TextBlock Text="{Binding OAuthModeTitle}" FontSize="12" />
5763
</TabItem.Header>
5864
<StackPanel Margin="0,10">
5965
<Button x:Name="signInBrowserButton"
@@ -64,6 +70,12 @@
6470
HorizontalAlignment="Center"
6571
Margin="0,0,0,10"
6672
Classes="accent"/>
73+
<Button x:Name="signInDeviceButton"
74+
Content="Sign in with a code"
75+
Command="{Binding SignInDeviceCommand}"
76+
IsVisible="{Binding ShowDeviceLogin}"
77+
HorizontalAlignment="Center"
78+
Margin="0,10,0,10"/>
6779
</StackPanel>
6880
</TabItem>
6981

src/shared/GitHub.UI.Avalonia/Views/CredentialsView.axaml.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class CredentialsView : UserControl, IFocusable
99
{
1010
private TabControl _tabControl;
1111
private Button _browserButton;
12+
private Button _deviceButton;
1213
private TextBox _tokenTextBox;
1314
private TextBox _userNameTextBox;
1415
private TextBox _passwordTextBox;
@@ -24,6 +25,7 @@ private void InitializeComponent()
2425

2526
_tabControl = this.FindControl<TabControl>("authModesTabControl");
2627
_browserButton = this.FindControl<Button>("signInBrowserButton");
28+
_deviceButton = this.FindControl<Button>("signInDeviceButton");
2729
_tokenTextBox = this.FindControl<TextBox>("tokenTextBox");
2830
_userNameTextBox = this.FindControl<TextBox>("userNameTextBox");
2931
_passwordTextBox = this.FindControl<TextBox>("passwordTextBox");
@@ -43,6 +45,11 @@ public void SetFocus()
4345
_tabControl.SelectedIndex = 0;
4446
_browserButton.Focus();
4547
}
48+
else if (vm.ShowDeviceLogin)
49+
{
50+
_tabControl.SelectedIndex = 0;
51+
_deviceButton.Focus();
52+
}
4653
else if (vm.ShowTokenLogin)
4754
{
4855
_tabControl.SelectedIndex = 1;

0 commit comments

Comments
 (0)