Skip to content

Commit

Permalink
Merge pull request #212 from mjcheetham/msauth-choice
Browse files Browse the repository at this point in the history
Allow user to override which interactive authentication flow used for MS auth
  • Loading branch information
mjcheetham committed Nov 9, 2020
2 parents b5a15d8 + 151fec2 commit df90676
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 30 deletions.
43 changes: 43 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,46 @@ git config --global credential.plaintextStorePath /mnt/external-drive/credential
```

**Also see: [GCM_PLAINTEXT_STORE_PATH](environment.md#GCM_PLAINTEXT_STORE_PATH)**

---

### credential.msauthFlow

Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required.

Defaults to the value `auto`.

**Note:** This setting will be ignored if a native authentication helper is configured and available. See [`credential.msauthHelper`](#credentialmsauthhelper) for more information.

Value|Credential Store
-|-
`auto` _(default)_|Select the best option depending on the current environment and platform.
`embedded`|Show a window with embedded web view control.
`system`|Open the user's default web browser.
`devicecode`|Show a device code.

#### Example

```shell
git config --global credential.msauthFlow devicecode
```

**Also see: [GCM_MSAUTH_FLOW](environment.md#GCM_MSAUTH_FLOW)**

---

### credential.msauthHelper

Full path to an external 'helper' tool to which Microsoft authentication should be delegated.

On macOS this defaults to the included native `Microsoft.Authentication.Helper` tool. On all other platforms this is not set.

**Note:** If a helper is set and available then all Microsoft authentication will be delegated to this helper and the [`credential.msauthFlow`](#credentialmsauthflow) setting will be ignored. Setting the value to the empty string (`""`) will unset any default helper.

#### Example

```shell
git config --global credential.msauthHelper "C:\path\to\helper.exe"
```

**Also see: [GCM_MSAUTH_HELPER](environment.md#GCM_MSAUTH_HELPER)**
55 changes: 55 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,58 @@ export GCM_PLAINTEXT_STORE_PATH=/mnt/external-drive/credentials
```

**Also see: [credential.plaintextStorePath](configuration.md#credentialplaintextstorepath)**

---

### GCM_MSAUTH_FLOW

Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required.

Defaults to the value `auto`.

**Note:** This setting will be ignored if a native authentication helper is configured and available. See [`GCM_MSAUTH_HELPER`](#gcm_msauth_helper) for more information.

Value|Credential Store
-|-
`auto` _(default)_|Select the best option depending on the current environment and platform.
`embedded`|Show a window with embedded web view control.
`system`|Open the user's default web browser.
`devicecode`|Show a device code.

##### Windows

```batch
SET GCM_MSAUTH_FLOW="devicecode"
```

##### macOS/Linux

```bash
export GCM_MSAUTH_FLOW="devicecode"
```

**Also see: [credential.msauthFlow](configuration.md#credentialmsauthflow)**

---

### GCM_MSAUTH_HELPER

Full path to an external 'helper' tool to which Microsoft authentication should be delegated.

On macOS this defaults to the included native `Microsoft.Authentication.Helper` tool. On all other platforms this is not set.

**Note:** If a helper is set and available then all Microsoft authentication will be delegated to this helper and the [`GCM_MSAUTH_FLOW`](#gcm_msauth_flow) setting will be ignored. Setting the value to the empty string (`""`) will unset any default helper.

##### Windows

```batch
SET GCM_MSAUTH_HELPER="C:\path\to\helper.exe"
```

##### macOS/Linux

```bash
export GCM_MSAUTH_HELPER="/usr/local/bin/msauth-helper"
```

**Also see: [credential.msauthHelper](configuration.md#credentialmsauthhelper)**
5 changes: 2 additions & 3 deletions src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ internal static class AzureDevOpsConstants
// We share this to be able to consume existing access tokens from the VS caches
public const string AadClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";

// Standard redirect URI for native client 'v1 protocol' applications
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#request-an-authorization-code
public static readonly Uri AadRedirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob");
// Redirect URI specified by the Visual Studio application configuration
public static readonly Uri AadRedirectUri = new Uri("http://localhost");

public const string VstsHostSuffix = ".visualstudio.com";
public const string AzureDevOpsHost = "dev.azure.com";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ protected bool TryFindHelperExecutablePath(string envar, string configName, stri
helperName = PlatformUtils.IsWindows() ? $"{defaultValue}.exe" : defaultValue;
}

// If the user set the helper override to the empty string then they are signalling not to use a helper
if (string.IsNullOrEmpty(helperName))
{
path = null;
return false;
}

if (Path.IsPathRooted(helperName))
{
path = helperName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
Expand All @@ -17,6 +18,14 @@ public interface IMicrosoftAuthentication
Uri remoteUri, string userName);
}

public enum MicrosoftAuthenticationFlowType
{
Auto = 0,
EmbeddedWebView = 1,
SystemWebView = 2,
DeviceCode = 3
}

public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthentication
{
public static readonly string[] AuthorityIds =
Expand Down Expand Up @@ -97,7 +106,8 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
// If we failed to acquire an AT silently (either because we don't have an existing user, or the user's RT has expired)
// we need to prompt the user for credentials.
//
// Depending on the current platform and session type we try to show the most appropriate authentication interface:
// If the user has expressed a preference in how the want to perform the interactive authentication flows then we respect that.
// Otherwise, depending on the current platform and session type we try to show the most appropriate authentication interface:
//
// On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. For Windows + .NET Framework this is the
// best and natural experience.
Expand All @@ -111,41 +121,86 @@ private async Task<JsonWebToken> GetAccessTokenInProcAsync(string authority, str
//
// The device code flow has no limitations other than a way to communicate to the user the code required to authenticate.
//

// If the user has disabled interaction all we can do is fail at this point
ThrowIfUserInteractionDisabled();

if (result is null)
{
#if NETFRAMEWORK
// If we're in an interactive session and on .NET Framework, let MSAL show the WinForms-based embeded UI
if (Context.SessionManager.IsDesktopSession)
// If the user has disabled interaction all we can do is fail at this point
ThrowIfUserInteractionDisabled();

// Check for a user flow preference
MicrosoftAuthenticationFlowType flowType = GetFlowType();
switch (flowType)
{
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.WithUseEmbeddedWebView(true)
.ExecuteAsync();
case MicrosoftAuthenticationFlowType.Auto:
if (CanUseEmbeddedWebView())
goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;

if (CanUseSystemWebView(app, redirectUri))
goto case MicrosoftAuthenticationFlowType.SystemWebView;

// Fall back to device code flow
goto case MicrosoftAuthenticationFlowType.DeviceCode;

case MicrosoftAuthenticationFlowType.EmbeddedWebView:
Context.Trace.WriteLine("Performing interactive auth with embedded web view...");
EnsureCanUseEmbeddedWebView();
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.WithUseEmbeddedWebView(true)
.ExecuteAsync();
break;

case MicrosoftAuthenticationFlowType.SystemWebView:
Context.Trace.WriteLine("Performing interactive auth with system web view...");
EnsureCanUseSystemWebView(app, redirectUri);
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.WithSystemWebViewOptions(GetSystemWebViewOptions())
.ExecuteAsync();
break;

case MicrosoftAuthenticationFlowType.DeviceCode:
Context.Trace.WriteLine("Performing interactive auth with device code...");
// We don't have a way to display a device code without a terminal at the moment
// TODO: introduce a small GUI window to show a code if no TTY exists
ThrowIfTerminalPromptsDisabled();
result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
break;

default:
goto case MicrosoftAuthenticationFlowType.Auto;
}
#elif NETSTANDARD
// MSAL requires the application redirect URI is a loopback address to use the System WebView
if (Context.SessionManager.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback)
}

return new JsonWebToken(result.AccessToken);
}

private MicrosoftAuthenticationFlowType GetFlowType()
{
if (Context.Settings.TryGetSetting(
Constants.EnvironmentVariables.MsAuthFlow,
Constants.GitConfiguration.Credential.SectionName,
Constants.GitConfiguration.Credential.MsAuthFlow,
out string valueStr))
{
Context.Trace.WriteLine($"Microsoft auth flow overriden to '{valueStr}'.");
switch (valueStr.ToLowerInvariant())
{
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.WithSystemWebViewOptions(GetSystemWebViewOptions())
.ExecuteAsync();
case "auto":
return MicrosoftAuthenticationFlowType.Auto;
case "embedded":
return MicrosoftAuthenticationFlowType.EmbeddedWebView;
case "system":
return MicrosoftAuthenticationFlowType.SystemWebView;
default:
if (Enum.TryParse(valueStr, ignoreCase: true, out MicrosoftAuthenticationFlowType value))
return value;
break;
}
#endif
// If we do not have a way to show a GUI, use device code flow over the TTY
else
{
ThrowIfTerminalPromptsDisabled();

result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
}
Context.Streams.Error.WriteLine($"warning: unknown Microsoft Authentication flow type '{valueStr}'; using 'auto'");
}

return new JsonWebToken(result.AccessToken);
return MicrosoftAuthenticationFlowType.Auto;
}

/// <summary>
Expand Down Expand Up @@ -274,5 +329,55 @@ public HttpClient GetHttpClient()
}

#endregion

#region Auth flow capability detection

private bool CanUseEmbeddedWebView()
{
// If we're in an interactive session and on .NET Framework then MSAL can show the WinForms-based embedded UI
#if NETFRAMEWORK
return Context.SessionManager.IsDesktopSession;
#else
return false;
#endif
}

private void EnsureCanUseEmbeddedWebView()
{
#if NETFRAMEWORK
if (!Context.SessionManager.IsDesktopSession)
{
throw new InvalidOperationException("Embedded web view is not available without a desktop session.");
}
#else
throw new InvalidOperationException("Embedded web view is not available on .NET Core.");
#endif
}

private bool CanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)
{
// MSAL requires the application redirect URI is a loopback address to use the System WebView
return Context.SessionManager.IsDesktopSession && app.IsSystemWebViewAvailable && redirectUri.IsLoopback;
}

private void EnsureCanUseSystemWebView(IPublicClientApplication app, Uri redirectUri)
{
if (!Context.SessionManager.IsDesktopSession)
{
throw new InvalidOperationException("System web view is not available without a desktop session.");
}

if (!app.IsSystemWebViewAvailable)
{
throw new InvalidOperationException("System web view is not available on this platform.");
}

if (!redirectUri.IsLoopback)
{
throw new InvalidOperationException("System web view is not available for this service configuration.");
}
}

#endregion
}
}
2 changes: 2 additions & 0 deletions src/shared/Microsoft.Git.CredentialManager/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static class EnvironmentVariables
public const string GitSslNoVerify = "GIT_SSL_NO_VERIFY";
public const string GcmInteractive = "GCM_INTERACTIVE";
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
public const string MsAuthFlow = "GCM_MSAUTH_FLOW";
public const string MsAuthHelper = "GCM_MSAUTH_HELPER";
public const string GcmCredNamespace = "GCM_NAMESPACE";
public const string GcmCredentialStore = "GCM_CREDENTIAL_STORE";
Expand Down Expand Up @@ -80,6 +81,7 @@ public static class Credential
public const string HttpsProxy = "httpsProxy";
public const string UseHttpPath = "useHttpPath";
public const string Interactive = "interactive";
public const string MsAuthFlow = "msauthFlow";
public const string MsAuthHelper = "msauthHelper";
public const string CredNamespace = "namespace";
public const string CredentialStore = "credentialStore";
Expand Down

0 comments on commit df90676

Please sign in to comment.