Skip to content

Commit

Permalink
msauth: allow user to select auth flow type
Browse files Browse the repository at this point in the history
Allow the user to override which Microsoft authentication flow to use
when interaction is required.

The options available are:
 - embedded web view
 - system web view
 - device code

Previously we always picked one of these flows (or used a native
external helper) to use. In some instances it may be desirable for the
user to force a particular flow, for instance to take advantage of
browser sign-in state or OS integrations by using 'system'.

If no option is set, or `auto` is selected the existing behaviour is
preserved (we pick for the user).
  • Loading branch information
mjcheetham committed Nov 9, 2020
1 parent b5a15d8 commit 5968b55
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 27 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)**
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 5968b55

Please sign in to comment.