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

Commit f78f591

Browse files
authored
feat: add national cloud support (#332)
feat: add national cloud support Allows logging in to national clouds using the `--environment` CLI option. See microsoftgraph/msgraph-cli#396 perf: enable concurrent io when clearing the token cache
1 parent e9b73fc commit f78f591

File tree

13 files changed

+192
-62
lines changed

13 files changed

+192
-62
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,6 @@ FodyWeavers.xsd
453453
*.sln.iml
454454

455455
### VisualStudio Patch ###
456-
# Additional files built by Visual Studio
456+
# Additional files built by Visual Studio
457+
458+
.env.local

.vscode/launch.json

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"request": "launch",
1111
"preLaunchTask": "build sample",
1212
// If you have changed target frameworks, make sure to update the program path.
13-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
13+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
1414
"args": [
1515
"login",
1616
"--strategy",
@@ -30,7 +30,7 @@
3030
"request": "launch",
3131
"preLaunchTask": "build sample",
3232
// If you have changed target frameworks, make sure to update the program path.
33-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
33+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
3434
"args": [
3535
"login",
3636
"--strategy",
@@ -46,7 +46,7 @@
4646
"type": "coreclr",
4747
"request": "launch",
4848
"preLaunchTask": "build sample",
49-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
49+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
5050
"args": [
5151
"login",
5252
"--strategy",
@@ -67,7 +67,7 @@
6767
"type": "coreclr",
6868
"request": "launch",
6969
"preLaunchTask": "build sample",
70-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
70+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
7171
"args": [
7272
"login",
7373
"--strategy",
@@ -77,6 +77,23 @@
7777
"--client-id",
7878
"e49807f2-94cc-4f59-9e14-be2a37eab7c2"
7979
],
80+
"envFile": "${workspaceFolder}/.env.local",
81+
"cwd": "${workspaceFolder}/src/sample",
82+
"console": "internalConsole",
83+
"stopAtEntry": false
84+
},
85+
{
86+
"name": "login national cloud (sample)",
87+
"type": "coreclr",
88+
"request": "launch",
89+
"preLaunchTask": "build sample",
90+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
91+
"args": [
92+
"login",
93+
"--environment",
94+
"US_GOV"
95+
],
96+
"envFile": "${workspaceFolder}/.env.local",
8097
"cwd": "${workspaceFolder}/src/sample",
8198
"console": "internalConsole",
8299
"stopAtEntry": false
@@ -86,13 +103,32 @@
86103
"type": "coreclr",
87104
"request": "launch",
88105
"preLaunchTask": "build sample",
89-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
106+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
90107
"args": [
91108
"users",
92109
"list",
93110
"--debug",
94111
"--top",
95-
"2"
112+
"2",
113+
"--headers",
114+
"sample=header"
115+
],
116+
"envFile": "${workspaceFolder}/.env.local",
117+
"cwd": "${workspaceFolder}/src/sample",
118+
"console": "integratedTerminal",
119+
"stopAtEntry": false,
120+
"justMyCode": false
121+
},
122+
{
123+
"name": "me get (sample)",
124+
"type": "coreclr",
125+
"request": "launch",
126+
"preLaunchTask": "build sample",
127+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
128+
"args": [
129+
"me",
130+
"get",
131+
"--debug"
96132
],
97133
"cwd": "${workspaceFolder}/src/sample",
98134
"console": "integratedTerminal",
@@ -103,7 +139,7 @@
103139
"type": "coreclr",
104140
"request": "launch",
105141
"preLaunchTask": "build sample",
106-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
142+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
107143
"args": [
108144
"logout",
109145
"--debug"
@@ -117,9 +153,8 @@
117153
"type": "coreclr",
118154
"request": "launch",
119155
"preLaunchTask": "build sample",
120-
"program": "${workspaceFolder}/src/sample/bin/Debug/net7.0/sample.dll",
156+
"program": "${workspaceFolder}/src/sample/bin/Debug/net8.0/sample.dll",
121157
"args": [
122-
"login",
123158
"--help"
124159
],
125160
"cwd": "${workspaceFolder}/src/sample",
@@ -132,4 +167,4 @@
132167
"request": "attach"
133168
}
134169
]
135-
}
170+
}

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

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ public AuthenticationServiceFactory(IPathUtility pathUtility, IAuthenticationCac
4141
/// <param name="clientId">Client Id</param>
4242
/// <param name="certificateName">Certificate name</param>
4343
/// <param name="certificateThumbPrint">Certificate thumb-print</param>
44+
/// <param name="environment">The national cloud environment. Either 'Global', 'US_GOV', 'US_GOV_DOD' or 'China'</param>
4445
/// <param name="cancellationToken">Cancellation token</param>
4546
/// <returns>Returns a login service instance.</returns>
4647
/// <exception cref="InvalidOperationException">When an unsupported authentication strategy is provided.</exception>
47-
public virtual async Task<LoginServiceBase> GetAuthenticationServiceAsync(AuthenticationStrategy strategy, string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, CancellationToken cancellationToken = default)
48+
public virtual async Task<LoginServiceBase> GetAuthenticationServiceAsync(AuthenticationStrategy strategy, string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, CloudEnvironment environment, CancellationToken cancellationToken = default)
4849
{
49-
var credential = await GetTokenCredentialAsync(strategy, tenantId, clientId, certificateName, certificateThumbPrint, cancellationToken);
50+
var credential = await GetTokenCredentialAsync(strategy, tenantId, clientId, certificateName, certificateThumbPrint, environment, cancellationToken);
5051
if (strategy == AuthenticationStrategy.DeviceCode && credential is DeviceCodeCredential deviceCred)
5152
{
5253
return new InteractiveLoginService<DeviceCodeCredential>(deviceCred, pathUtility);
@@ -81,35 +82,33 @@ public virtual async Task<LoginServiceBase> GetAuthenticationServiceAsync(Authen
8182
/// <param name="clientId">Client Id</param>
8283
/// <param name="certificateName">Certificate name</param>
8384
/// <param name="certificateThumbPrint">Certificate thumb-print</param>
85+
/// <param name="environment">The cloud environment. <see cref="CloudEnvironment"/></param>
8486
/// <param name="cancellationToken">Cancellation token.</param>
8587
/// <returns>A token credential instance.</returns>
8688
/// <exception cref="InvalidOperationException">When an unsupported authentication strategy is provided.</exception>
87-
public virtual async Task<TokenCredential> GetTokenCredentialAsync(AuthenticationStrategy strategy, string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, CancellationToken cancellationToken = default)
89+
/// <exception cref="ArgumentNullException">When a null url is provided for the authority host.</exception>
90+
public virtual async Task<TokenCredential> GetTokenCredentialAsync(AuthenticationStrategy strategy, string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, CloudEnvironment environment, CancellationToken cancellationToken = default)
8891
{
89-
switch (strategy)
92+
var authorityHost = environment.Authority();
93+
return strategy switch
9094
{
91-
case AuthenticationStrategy.DeviceCode:
92-
return await GetDeviceCodeCredentialAsync(tenantId, clientId, cancellationToken);
93-
case AuthenticationStrategy.InteractiveBrowser:
94-
return await GetInteractiveBrowserCredentialAsync(tenantId, clientId, cancellationToken);
95-
case AuthenticationStrategy.ClientCertificate:
96-
return GetClientCertificateCredential(tenantId, clientId, certificateName, certificateThumbPrint);
97-
case AuthenticationStrategy.Environment:
98-
return new EnvironmentCredential(tenantId, clientId);
99-
case AuthenticationStrategy.ManagedIdentity:
100-
return new ManagedIdentityCredential(clientId);
101-
default:
102-
throw new InvalidOperationException($"The authentication strategy {strategy} is not supported");
103-
}
95+
AuthenticationStrategy.DeviceCode => await GetDeviceCodeCredentialAsync(tenantId, clientId, authorityHost, cancellationToken),
96+
AuthenticationStrategy.InteractiveBrowser => await GetInteractiveBrowserCredentialAsync(tenantId, clientId, authorityHost, cancellationToken),
97+
AuthenticationStrategy.ClientCertificate => GetClientCertificateCredential(tenantId, clientId, certificateName, certificateThumbPrint, authorityHost),
98+
AuthenticationStrategy.Environment => new EnvironmentCredential(tenantId, clientId, new TokenCredentialOptions { AuthorityHost = authorityHost }),
99+
AuthenticationStrategy.ManagedIdentity => new ManagedIdentityCredential(clientId, new TokenCredentialOptions { AuthorityHost = authorityHost }),
100+
_ => throw new InvalidOperationException($"The authentication strategy {strategy} is not supported"),
101+
};
104102
}
105103

106-
private async Task<DeviceCodeCredential> GetDeviceCodeCredentialAsync(string? tenantId, string? clientId, CancellationToken cancellationToken = default)
104+
private async Task<DeviceCodeCredential> GetDeviceCodeCredentialAsync(string? tenantId, string? clientId, Uri authorityHost, CancellationToken cancellationToken = default)
107105
{
108106
DeviceCodeCredentialOptions credOptions = new()
109107
{
110108
ClientId = clientId ?? Constants.DefaultAppId,
111109
TenantId = tenantId ?? Constants.DefaultTenant,
112110
DisableAutomaticAuthentication = true,
111+
AuthorityHost = authorityHost
113112
};
114113

115114
TokenCachePersistenceOptions tokenCacheOptions = new() { Name = Constants.TokenCacheName };
@@ -119,13 +118,14 @@ private async Task<DeviceCodeCredential> GetDeviceCodeCredentialAsync(string? te
119118
return new DeviceCodeCredential(credOptions);
120119
}
121120

122-
private async Task<InteractiveBrowserCredential> GetInteractiveBrowserCredentialAsync(string? tenantId, string? clientId, CancellationToken cancellationToken = default)
121+
private async Task<InteractiveBrowserCredential> GetInteractiveBrowserCredentialAsync(string? tenantId, string? clientId, Uri authorityHost, CancellationToken cancellationToken = default)
123122
{
124123
InteractiveBrowserCredentialOptions credOptions = new()
125124
{
126125
ClientId = clientId ?? Constants.DefaultAppId,
127126
TenantId = tenantId ?? Constants.DefaultTenant,
128127
DisableAutomaticAuthentication = true,
128+
AuthorityHost = authorityHost
129129
};
130130

131131
TokenCachePersistenceOptions tokenCacheOptions = new() { Name = Constants.TokenCacheName };
@@ -135,8 +135,8 @@ private async Task<InteractiveBrowserCredential> GetInteractiveBrowserCredential
135135
return new InteractiveBrowserCredential(credOptions);
136136
}
137137

138-
private ClientCertificateCredential GetClientCertificateCredential(string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint)
138+
private ClientCertificateCredential GetClientCertificateCredential(string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, Uri authorityHost)
139139
{
140-
return ClientCertificateCredentialFactory.GetClientCertificateCredential(tenantId ?? Constants.DefaultTenant, clientId ?? Constants.DefaultAppId, certificateName, certificateThumbPrint);
140+
return ClientCertificateCredentialFactory.GetClientCertificateCredential(tenantId ?? Constants.DefaultTenant, clientId ?? Constants.DefaultAppId, certificateName, certificateThumbPrint, authorityHost);
141141
}
142142
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Security.Cryptography;
55
using System.Security.Cryptography.X509Certificates;
66
using Azure.Identity;
7+
using Microsoft.Graph.Cli.Core.Utils;
78

89
namespace Microsoft.Graph.Cli.Core.Authentication;
910

@@ -19,15 +20,17 @@ public static class ClientCertificateCredentialFactory
1920
/// <param name="clientId">ClientId</param>
2021
/// <param name="certificateName">Subject name of the certificate.</param>
2122
/// <param name="certificateThumbPrint">Thumb print of the certificate.</param>
23+
/// <param name="authorityHost">The entra authentication endpoint (to use with national clouds)</param>
2224
/// <returns>A ClientCertificateCredential</returns>
23-
public static ClientCertificateCredential GetClientCertificateCredential(string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint)
25+
/// <exception cref="ArgumentNullException">When a null url is provided for the authority host.</exception>
26+
public static ClientCertificateCredential GetClientCertificateCredential(string? tenantId, string? clientId, string? certificateName, string? certificateThumbPrint, Uri authorityHost)
2427
{
2528
if (string.IsNullOrWhiteSpace(certificateName) && string.IsNullOrWhiteSpace(certificateThumbPrint))
2629
{
2730
throw new ArgumentException("Either a certificate name or a certificate thumb print must be provided.");
2831
}
2932

30-
ClientCertificateCredentialOptions credOptions = new();
33+
ClientCertificateCredentialOptions credOptions = new() { AuthorityHost = authorityHost };
3134

3235
// // TODO: Enable token caching
3336
// // Fix error:
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using Azure.Identity;
3+
using Microsoft.Graph;
4+
using Microsoft.Graph.Cli.Core.Utils;
5+
6+
7+
/// <summary>
8+
/// The cloud environment to use
9+
/// </summary>
10+
public enum CloudEnvironment
11+
{
12+
/// <summary>
13+
/// Global environment.
14+
/// </summary>
15+
Global,
16+
/// <summary>
17+
/// US Government cloud environment.
18+
/// </summary>
19+
USGov,
20+
/// <summary>
21+
/// US Government Department of Defense (DoD) cloud environment.
22+
/// </summary>
23+
USGovDoD,
24+
/// <summary>
25+
/// China cloud environment.
26+
/// </summary>
27+
China,
28+
}
29+
30+
/// <summary>
31+
/// Provides methods for the <see cref="CloudEnvironment"/> class.
32+
/// </summary>
33+
public static class CloudEnvironmentExtensions
34+
{
35+
/// <summary>
36+
/// Gets the authority URL for the specified cloud environment.
37+
/// </summary>
38+
/// <param name="environment">The cloud environment.</param>
39+
/// <returns>The authority URL.</returns>
40+
/// <exception cref="ArgumentException">
41+
/// If the cloud environment is not one of the <see cref="CloudEnvironment"/> members.
42+
/// </exception>
43+
public static Uri Authority(this CloudEnvironment environment)
44+
{
45+
return environment switch
46+
{
47+
CloudEnvironment.Global => AzureAuthorityHosts.AzurePublicCloud,
48+
CloudEnvironment.USGov or CloudEnvironment.USGovDoD => AzureAuthorityHosts.AzureGovernment,
49+
CloudEnvironment.China => AzureAuthorityHosts.AzureChina,
50+
_ => throw new ArgumentException("Unknown cloud environment", nameof(environment))
51+
};
52+
}
53+
54+
/// <summary>
55+
/// Gets the GraphClient Cloud identifier.
56+
/// </summary>
57+
/// <param name="environment">The cloud environment.</param>
58+
/// <returns>The cloud identifier to be used by the graph client.</returns>
59+
/// <exception cref="ArgumentException">
60+
/// If the cloud environment is not one of the <see cref="CloudEnvironment"/> members.
61+
/// </exception>
62+
public static string GraphClientCloud(this CloudEnvironment environment)
63+
{
64+
return environment switch
65+
{
66+
CloudEnvironment.Global => GraphClientFactory.Global_Cloud,
67+
CloudEnvironment.USGov => GraphClientFactory.USGOV_Cloud,
68+
CloudEnvironment.USGovDoD => GraphClientFactory.USGOV_DOD_Cloud,
69+
CloudEnvironment.China => GraphClientFactory.China_Cloud,
70+
_ => throw new ArgumentException("Unknown cloud environment", nameof(environment))
71+
};
72+
}
73+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ public EnvironmentCredential(string? tenantId, string? clientId, TokenCredential
7272
bool sendCertificateChain = !string.IsNullOrEmpty(clientSendCertificateChain) &&
7373
(clientSendCertificateChain == "1" || clientSendCertificateChain == "true");
7474

75-
ClientCertificateCredentialOptions clientCertificateCredentialOptions = new ClientCertificateCredentialOptions
75+
ClientCertificateCredentialOptions clientCertificateCredentialOptions = new()
7676
{
7777
AuthorityHost = _options.AuthorityHost,
7878
Transport = _options.Transport,
79-
SendCertificateChain = sendCertificateChain
79+
SendCertificateChain = sendCertificateChain,
8080
};
8181
// Use reflection to set internal properties.
8282
X509Certificate2? cert;

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public sealed class LoginCommand : Command
2727

2828
private Option<string> certificateThumbPrintOption = new("--certificate-thumb-print", "The thumbprint of your certificate. The certificate will be retrieved from the current user's certificate store.");
2929

30+
private Option<CloudEnvironment> environmentOption = new("--environment", () => CloudEnvironment.Global, "Select the cloud environment to log in to. If login is run without providing an environment, Global is used.");
31+
3032
private Option<AuthenticationStrategy> strategyOption = new("--strategy", () => Constants.defaultAuthStrategy);
3133

3234
internal LoginCommand() : base("login", "Login and store the session for use in subsequent commands")
@@ -36,6 +38,7 @@ public sealed class LoginCommand : Command
3638
AddOption(tenantIdOption);
3739
AddOption(certificateNameOption);
3840
AddOption(certificateThumbPrintOption);
41+
AddOption(environmentOption);
3942
AddOption(strategyOption);
4043
this.SetHandler(async (context) =>
4144
{
@@ -44,15 +47,16 @@ public sealed class LoginCommand : Command
4447
var tenantId = context.ParseResult.GetValueForOption(tenantIdOption);
4548
var certificateName = context.ParseResult.GetValueForOption(certificateNameOption);
4649
var certificateThumbPrint = context.ParseResult.GetValueForOption(certificateThumbPrintOption);
50+
var environment = context.ParseResult.GetValueForOption(environmentOption);
4751
var strategy = context.ParseResult.GetValueForOption(strategyOption);
4852
var cancellationToken = context.GetCancellationToken();
4953

5054
var authUtil = context.BindingContext.GetRequiredService<IAuthenticationCacheManager>();
5155
var authSvcFactory = context.BindingContext.GetRequiredService<AuthenticationServiceFactory>();
5256

53-
var authService = await authSvcFactory.GetAuthenticationServiceAsync(strategy, tenantId, clientId, certificateName, certificateThumbPrint, cancellationToken);
57+
var authService = await authSvcFactory.GetAuthenticationServiceAsync(strategy, tenantId, clientId, certificateName, certificateThumbPrint, environment, cancellationToken);
5458
await authService.LoginAsync(scopes, cancellationToken);
55-
await authUtil.SaveAuthenticationIdentifiersAsync(clientId, tenantId, certificateName, certificateThumbPrint, strategy, cancellationToken);
59+
await authUtil.SaveAuthenticationIdentifiersAsync(clientId, tenantId, certificateName, certificateThumbPrint, strategy, environment, cancellationToken);
5660
});
5761
}
5862

0 commit comments

Comments
 (0)