Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit a5fe538

Browse files
authored
feat: add web account manager (WAM) support (#436)
chore: remove redundant attribute fix: add default scopes to solve login error when no scopes are provided ci: fixes test failure chore: remove redundant SupportedOSPlatform attribute
1 parent e7d7682 commit a5fe538

File tree

6 files changed

+73
-12
lines changed

6 files changed

+73
-12
lines changed

src/Microsoft.Graph.Cli.Core.Tests/Commands/Authentication/LoginCommandTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public void Parses_No_Scopes()
2323
var scopes = result.FindResultFor(command.Options[0])?.Tokens.Select(t => t.Value);
2424

2525
// Then
26-
Assert.Null(scopes);
26+
Assert.NotNull(scopes);
27+
Assert.Empty(scopes);
2728
}
2829

2930
[Fact]

src/Microsoft.Graph.Cli.Core/Authentication/AuthenticationServiceFactory.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99

10+
#if OS_WINDOWS
11+
using System.Diagnostics;
12+
using Azure.Identity.Broker;
13+
using Microsoft.Graph.Cli.Core.utils;
14+
#endif
15+
1016
namespace Microsoft.Graph.Cli.Core.Authentication;
1117

1218
/// <summary>
@@ -120,13 +126,17 @@ private async Task<DeviceCodeCredential> GetDeviceCodeCredentialAsync(string? te
120126

121127
private async Task<InteractiveBrowserCredential> GetInteractiveBrowserCredentialAsync(string? tenantId, string? clientId, Uri authorityHost, CancellationToken cancellationToken = default)
122128
{
123-
InteractiveBrowserCredentialOptions credOptions = new()
124-
{
125-
ClientId = clientId ?? Constants.DefaultAppId,
126-
TenantId = tenantId ?? Constants.DefaultTenant,
127-
DisableAutomaticAuthentication = true,
128-
AuthorityHost = authorityHost
129-
};
129+
#if OS_WINDOWS
130+
Debug.Assert(OperatingSystem.IsWindows());
131+
InteractiveBrowserCredentialBrokerOptions credOptions = new(WindowUtils.GetConsoleOrTerminalWindow());
132+
#else
133+
InteractiveBrowserCredentialOptions credOptions = new();
134+
#endif
135+
136+
credOptions.ClientId = clientId ?? Constants.DefaultAppId;
137+
credOptions.TenantId = tenantId ?? Constants.DefaultTenant;
138+
credOptions.DisableAutomaticAuthentication = true;
139+
credOptions.AuthorityHost = authorityHost;
130140

131141
TokenCachePersistenceOptions tokenCacheOptions = new() { Name = Constants.TokenCacheName };
132142
credOptions.TokenCachePersistenceOptions = tokenCacheOptions;

src/Microsoft.Graph.Cli.Core/Authentication/LoginServiceBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ public async Task LoginAsync(string[] scopes, CancellationToken cancellationToke
4848
{
4949
if (record is null) return;
5050
var recordPath = Path.Combine(pathUtility.GetApplicationDataDirectory(), Constants.AuthRecordPath);
51-
using var authRecordStream = new FileStream(recordPath, FileMode.Create, FileAccess.Write);
52-
await record.SerializeAsync(authRecordStream, cancellationToken);
51+
var authRecordStream = new FileStream(recordPath, FileMode.Create, FileAccess.Write);
52+
await using (authRecordStream.ConfigureAwait(false))
53+
{
54+
await record.SerializeAsync(authRecordStream, cancellationToken);
55+
}
5356
}
5457
}

src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Microsoft.Graph.Cli.Core.Commands.Authentication;
1313
/// </summary>
1414
public sealed class LoginCommand : Command
1515
{
16-
private Option<string[]> scopesOption = new("--scopes", "The login scopes e.g. User.Read. Required scopes can be found in the docs linked against each verb (get, list, create...) command.")
16+
private Option<string[]> scopesOption = new("--scopes",() => ["User.Read"], "The login scopes e.g. User.Read. Required scopes can be found in the docs linked against each verb (get, list, create...) command.")
1717
{
1818
Arity = ArgumentArity.ZeroOrMore,
1919
AllowMultipleArgumentsPerToken = true,

src/Microsoft.Graph.Cli.Core/Microsoft.Graph.Cli.Core.csproj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<IsTrimmable>true</IsTrimmable>
1212

1313
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
14+
15+
<!-- Needed to use LibraryImport -->
16+
<!-- see: https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation#differences-from-dllimport -->
17+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1418
</PropertyGroup>
1519

1620
<PropertyGroup>
@@ -39,9 +43,12 @@
3943
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
4044
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
4145
</PropertyGroup>
42-
46+
<PropertyGroup Condition="'$(OS)' == 'Windows_NT'">
47+
<DefineConstants>OS_WINDOWS</DefineConstants>
48+
</PropertyGroup>
4349
<ItemGroup>
4450
<PackageReference Include="Azure.Identity" Version="1.12.0"/>
51+
<PackageReference Include="Azure.Identity.Broker" Version="1.1.0" Condition="'$(OS)' == 'Windows_NT'" />
4552
<PackageReference Include="JmesPath.Net" Version="1.0.330"/>
4653
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/>
4754
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using System.Runtime.Versioning;
4+
5+
namespace Microsoft.Graph.Cli.Core.utils;
6+
7+
[SupportedOSPlatform("Windows")]
8+
internal partial class WindowUtils
9+
{
10+
enum GetAncestorFlags
11+
{
12+
GetParent = 1,
13+
GetRoot = 2,
14+
/// <summary>
15+
/// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent.
16+
/// </summary>
17+
GetRootOwner = 3
18+
}
19+
20+
/// <summary>
21+
/// Retrieves the handle to the ancestor of the specified window.
22+
/// </summary>
23+
/// <param name="hwnd">A handle to the window whose ancestor is to be retrieved.
24+
/// If this parameter is the desktop window, the function returns NULL. </param>
25+
/// <param name="flags">The ancestor to be retrieved.</param>
26+
/// <returns>The return value is the handle to the ancestor window.</returns>
27+
[LibraryImport("user32.dll")]
28+
private static partial IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);
29+
30+
[LibraryImport("kernel32.dll")]
31+
private static partial IntPtr GetConsoleWindow();
32+
33+
// https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/desktop-mobile/wam#parent-window-handles
34+
internal static IntPtr GetConsoleOrTerminalWindow()
35+
{
36+
var consoleHandle = GetConsoleWindow();
37+
var handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
38+
return handle;
39+
}
40+
}

0 commit comments

Comments
 (0)