Skip to content

Commit 66e3d90

Browse files
authored
Dynamically add broker authentication to the DAC chain if Broker package is referenced (Azure#49194)
1 parent 77ef895 commit 66e3d90

14 files changed

+273
-6
lines changed

sdk/identity/Azure.Identity.Broker/src/Azure.Identity.Broker.csproj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
77
<ApiCompatVersion>1.2.0</ApiCompatVersion>
88
<PackageTags>Microsoft Azure Identity Broker;$(PackageCommonTags)</PackageTags>
9-
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(RequiredTargetFrameworks);net462;net6.0</TargetFrameworks>
10-
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('OSX'))">$(RequiredTargetFrameworks);net6.0</TargetFrameworks>
11-
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Linux')) ">$(RequiredTargetFrameworks);net6.0</TargetFrameworks>
9+
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(RequiredTargetFrameworks);net462;net8.0</TargetFrameworks>
10+
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('OSX'))">$(RequiredTargetFrameworks);net8.0</TargetFrameworks>
11+
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Linux'))">$(RequiredTargetFrameworks);net8.0</TargetFrameworks>
1212
<NoWarn>$(NoWarn);3021</NoWarn>
1313
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1414
</PropertyGroup>
1515
<ItemGroup>
16-
<PackageReference Include="Azure.Identity" />
16+
<!-- <PackageReference Include="Azure.Identity" /> -->
17+
<ProjectReference Include="..\..\Azure.Identity\src\Azure.Identity.csproj" />
1718
<PackageReference Include="Microsoft.Identity.Client" />
1819
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
1920
<PackageReference Include="Microsoft.Identity.Client.Broker" />
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.Identity.Client;
7+
using Microsoft.Identity.Client.Broker;
8+
9+
namespace Azure.Identity.Broker
10+
{
11+
/// <summary>
12+
/// Options to configure the <see cref="InteractiveBrowserCredential"/> to use the system authentication broker in lieu of an embedded web view or the system browser.
13+
/// For more information, see <see href="https://aka.ms/azsdk/net/identity/interactive-brokered-auth">Interactive brokered authentication</see>.
14+
/// </summary>
15+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
16+
internal class DevelopmentBrokerOptions : InteractiveBrowserCredentialOptions, IMsalSettablePublicClientInitializerOptions, IMsalPublicClientInitializerOptions
17+
{
18+
private Action<PublicClientApplicationBuilder> _beforeBuildClient;
19+
/// <summary>
20+
/// Gets or sets whether Microsoft Account (MSA) passthrough is enabled.
21+
/// </summary>
22+
public bool? IsLegacyMsaPassthroughEnabled { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets whether to authenticate with the default broker account instead of prompting the user with a login dialog.
26+
/// </summary>
27+
public bool UseDefaultBrokerAccount { get; set; } = true;
28+
29+
/// <summary>
30+
/// Creates a new instance of <see cref="DevelopmentBrokerOptions"/> to configure a <see cref="InteractiveBrowserCredential"/> for broker authentication.
31+
/// </summary>
32+
public DevelopmentBrokerOptions() : base()
33+
{
34+
_beforeBuildClient = AddBroker;
35+
}
36+
37+
Action<PublicClientApplicationBuilder> IMsalSettablePublicClientInitializerOptions.BeforeBuildClient
38+
{
39+
get => _beforeBuildClient;
40+
set => _beforeBuildClient = value;
41+
}
42+
43+
Action<PublicClientApplicationBuilder> IMsalPublicClientInitializerOptions.BeforeBuildClient => _beforeBuildClient;
44+
45+
private void AddBroker(PublicClientApplicationBuilder builder)
46+
{
47+
builder.WithParentActivityOrWindow(() => IntPtr.Zero);
48+
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows);
49+
if (IsLegacyMsaPassthroughEnabled.HasValue)
50+
{
51+
options.MsaPassthrough = IsLegacyMsaPassthroughEnabled.Value;
52+
}
53+
builder.WithBroker(options);
54+
}
55+
}
56+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Core;
5+
using NUnit.Framework;
6+
7+
namespace Azure.Identity.Broker.Tests
8+
{
9+
public class DevelopmentBrokerOptionsTests
10+
{
11+
[Test]
12+
public void TryCreateDevelopmentBrokerOptions()
13+
{
14+
bool success = DefaultAzureCredentialFactory.TryCreateDevelopmentBrokerOptions(out var options);
15+
Assert.IsTrue(success, "Failed to create DevelopmentBrokerOptions.");
16+
Assert.IsNotNull(options, "DevelopmentBrokerOptions is null.");
17+
}
18+
19+
[Test]
20+
public void TryCreateDevelopmentBrokerOptionsFromCredentialFactory()
21+
{
22+
var factory = new DefaultAzureCredentialFactory(new DefaultAzureCredentialOptions());
23+
DefaultAzureCredentialFactory.TryCreateDevelopmentBrokerOptions(out var options);
24+
var cred = factory.CreateBrokerAuthenticationCredential(options);
25+
try
26+
{
27+
cred.GetToken(new TokenRequestContext(new[] { "https://management.azure.com/.default" }), default);
28+
}
29+
catch (CredentialUnavailableException)
30+
{
31+
// This is expected, as the broker is not available in the test environment.
32+
}
33+
}
34+
}
35+
}

sdk/identity/Azure.Identity/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- `DefaultAzureCredential` now includes silent authentication via the authentication broker on Windows if the `Azure.Identity.Broker` NuGet package is referenced. This allows for a more seamless authentication experience when using the `DefaultAzureCredential` in Windows environments. Setting the `ExcludeBrokerCredential` property on `DefaultAzureCredentialOptions` disables this feature.
8+
79
### Breaking Changes
810

911
### Bugs Fixed
@@ -16,6 +18,7 @@
1618
## 1.14.0-beta.2 (2025-03-11)
1719

1820
### Bugs Fixed
21+
1922
- `VisualStudioCredential` will now correctly fall through to the next credential in the chain when no account is found by Visual Studio. ([#48464](https://github.com/Azure/azure-sdk-for-net/issues/48464))
2023

2124
### Other Changes

sdk/identity/Azure.Identity/api/Azure.Identity.net8.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ public DefaultAzureCredentialOptions() { }
201201
public bool ExcludeAzureCliCredential { get { throw null; } set { } }
202202
public bool ExcludeAzureDeveloperCliCredential { get { throw null; } set { } }
203203
public bool ExcludeAzurePowerShellCredential { get { throw null; } set { } }
204+
public bool ExcludeBrokerCredential { get { throw null; } set { } }
204205
public bool ExcludeEnvironmentCredential { get { throw null; } set { } }
205206
public bool ExcludeInteractiveBrowserCredential { get { throw null; } set { } }
206207
public bool ExcludeManagedIdentityCredential { get { throw null; } set { } }

sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ public DefaultAzureCredentialOptions() { }
198198
public bool ExcludeAzureCliCredential { get { throw null; } set { } }
199199
public bool ExcludeAzureDeveloperCliCredential { get { throw null; } set { } }
200200
public bool ExcludeAzurePowerShellCredential { get { throw null; } set { } }
201+
public bool ExcludeBrokerCredential { get { throw null; } set { } }
201202
public bool ExcludeEnvironmentCredential { get { throw null; } set { } }
202203
public bool ExcludeInteractiveBrowserCredential { get { throw null; } set { } }
203204
public bool ExcludeManagedIdentityCredential { get { throw null; } set { } }

sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ public string VisualStudioCodeTenantId
260260
/// </summary>
261261
public bool ExcludeInteractiveBrowserCredential { get; set; } = true;
262262

263+
/// <summary>
264+
/// Specifies whether broker authentication, via <see cref="InteractiveBrowserCredential"/>, will be attempted as part of the <see cref="DefaultAzureCredential"/> authentication flow.
265+
/// Note that the broker authentication flow will only be attempted if the application has a reference to the Azure.Identity.Broker package.
266+
/// </summary>
267+
public bool ExcludeBrokerCredential { get; set; }
268+
263269
/// <summary>
264270
/// Specifies whether the <see cref="AzureCliCredential"/> will be excluded from the <see cref="DefaultAzureCredential"/> authentication flow.
265271
/// </summary>
@@ -318,6 +324,7 @@ public string VisualStudioCodeTenantId
318324
#pragma warning restore CS0618 // Type or member is obsolete
319325
dacClone.ExcludeAzurePowerShellCredential = ExcludeAzurePowerShellCredential;
320326
dacClone.IsForceRefreshEnabled = IsForceRefreshEnabled;
327+
dacClone.ExcludeBrokerCredential = ExcludeBrokerCredential;
321328
}
322329

323330
return clone;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Microsoft.Identity.Client;
6+
7+
namespace Azure.Identity
8+
{
9+
/// <summary>
10+
/// Options to configure the <see cref="InteractiveBrowserCredential"/> to use the system authentication broker in lieu of an embedded web view or the system browser.
11+
/// For more information, see <see href="https://aka.ms/azsdk/net/identity/interactive-brokered-auth">Interactive brokered authentication</see>.
12+
/// </summary>
13+
internal class DevelopmentBrokerOptions : InteractiveBrowserCredentialOptions, IMsalSettablePublicClientInitializerOptions, IMsalPublicClientInitializerOptions
14+
{
15+
private Action<PublicClientApplicationBuilder> _beforeBuildClient;
16+
/// <summary>
17+
/// Gets or sets whether Microsoft Account (MSA) passthrough is enabled.
18+
/// </summary>
19+
public bool? IsLegacyMsaPassthroughEnabled { get; set; } = true;
20+
21+
/// <summary>
22+
/// Gets or sets whether to authenticate with the default broker account instead of prompting the user with a login dialog.
23+
/// </summary>
24+
public bool UseDefaultBrokerAccount { get; set; } = true;
25+
26+
/// <summary>
27+
/// Creates a new instance of <see cref="DevelopmentBrokerOptions"/> to configure a <see cref="InteractiveBrowserCredential"/> for broker authentication.
28+
/// </summary>
29+
public DevelopmentBrokerOptions() : base()
30+
{
31+
}
32+
33+
Action<PublicClientApplicationBuilder> IMsalSettablePublicClientInitializerOptions.BeforeBuildClient
34+
{
35+
get => _beforeBuildClient;
36+
set => _beforeBuildClient = value;
37+
}
38+
39+
Action<PublicClientApplicationBuilder> IMsalPublicClientInitializerOptions.BeforeBuildClient => _beforeBuildClient;
40+
}
41+
}

sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredential.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class InteractiveBrowserCredential : TokenCredential
3131
internal string DefaultScope { get; }
3232
internal TenantIdResolverBase TenantIdResolver { get; }
3333
internal bool UseOperatingSystemAccount { get; }
34+
internal bool IsChainedCredential { get; set; }
3435

3536
private const string AuthenticationRequiredMessage = "Interactive authentication is needed to acquire token. Call Authenticate to interactively authenticate.";
3637
private const string NoDefaultScopeMessage = "Authenticating in this environment requires specifying a TokenRequestContext.";
@@ -96,6 +97,7 @@ internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCre
9697
Record = (options as InteractiveBrowserCredentialOptions)?.AuthenticationRecord;
9798
BrowserCustomization = (options as InteractiveBrowserCredentialOptions)?.BrowserCustomization;
9899
UseOperatingSystemAccount = (options as IMsalPublicClientInitializerOptions)?.UseDefaultBrokerAccount ?? false;
100+
IsChainedCredential = options?.IsChainedCredential ?? false;
99101
}
100102

101103
/// <summary>
@@ -243,6 +245,10 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
243245
}
244246
catch (MsalUiRequiredException e)
245247
{
248+
if (UseOperatingSystemAccount && IsChainedCredential)
249+
{
250+
throw;
251+
}
246252
inner = e;
247253
}
248254
}
@@ -256,7 +262,7 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
256262
}
257263
catch (Exception e)
258264
{
259-
throw scope.FailWrapAndThrow(e);
265+
throw scope.FailWrapAndThrow(e, null, IsChainedCredential);
260266
}
261267
}
262268

sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Reflection;
68
using Azure.Core;
79

810
namespace Azure.Identity
@@ -89,6 +91,11 @@ public TokenCredential[] CreateCredentialChain()
8991
chain.Add(CreateInteractiveBrowserCredential());
9092
}
9193

94+
if (!Options.ExcludeBrokerCredential && TryCreateDevelopmentBrokerOptions(out InteractiveBrowserCredentialOptions brokerOptions))
95+
{
96+
chain.Add(CreateBrokerAuthenticationCredential(brokerOptions));
97+
}
98+
9299
if (chain.Count == 0)
93100
{
94101
throw new ArgumentException("At least one credential type must be included in the authentication flow.", "options");
@@ -182,6 +189,23 @@ public virtual TokenCredential CreateInteractiveBrowserCredential()
182189
Pipeline);
183190
}
184191

192+
public TokenCredential CreateBrokerAuthenticationCredential(InteractiveBrowserCredentialOptions brokerOptions)
193+
{
194+
var options = Options.Clone<DevelopmentBrokerOptions>();
195+
((IMsalSettablePublicClientInitializerOptions)options).BeforeBuildClient = ((IMsalSettablePublicClientInitializerOptions)brokerOptions).BeforeBuildClient;
196+
197+
options.TokenCachePersistenceOptions = new TokenCachePersistenceOptions();
198+
199+
options.TenantId = Options.InteractiveBrowserTenantId;
200+
options.IsChainedCredential = true;
201+
202+
return new InteractiveBrowserCredential(
203+
Options.InteractiveBrowserTenantId,
204+
Options.InteractiveBrowserCredentialClientId ?? Constants.DeveloperSignOnClientId,
205+
options,
206+
Pipeline);
207+
}
208+
185209
public virtual TokenCredential CreateAzureDeveloperCliCredential()
186210
{
187211
var options = Options.Clone<AzureDeveloperCliCredentialOptions>();
@@ -232,5 +256,43 @@ public virtual TokenCredential CreateAzurePowerShellCredential()
232256

233257
return new AzurePowerShellCredential(options, Pipeline, default);
234258
}
259+
260+
/// <summary>
261+
/// Creates a DevelopmentBrokerOptions instance if the Azure.Identity.Broker assembly is loaded.
262+
/// This is used to enable broker authentication for development purposes.
263+
/// </summary>
264+
/// <param name="options"></param>
265+
/// <returns></returns>
266+
[UnconditionalSuppressMessage("Trimming", "IL2026",
267+
Justification = "Assembly.Load is used for optional functionality. If the assembly is trimmed, the catch block handles it gracefully.")]
268+
[UnconditionalSuppressMessage("Trimming", "IL2072",
269+
Justification = "Loading Azure.Identity.Broker assembly is optional, method handles missing assembly gracefully")]
270+
internal static bool TryCreateDevelopmentBrokerOptions(out InteractiveBrowserCredentialOptions options)
271+
{
272+
options = null;
273+
try
274+
{
275+
// Check if the Azure.Identity.Broker assembly is loaded
276+
Assembly brokerAssembly;
277+
brokerAssembly = Assembly.Load("Azure.Identity.Broker");
278+
279+
if (brokerAssembly != null)
280+
{
281+
// Get the DevelopmentBrokerOptions type
282+
var optionsType = brokerAssembly.GetType("Azure.Identity.Broker.DevelopmentBrokerOptions");
283+
if (optionsType != null)
284+
{
285+
// Create an instance using the parameterless constructor
286+
options = (InteractiveBrowserCredentialOptions)Activator.CreateInstance(optionsType);
287+
}
288+
}
289+
290+
return options != null;
291+
}
292+
catch
293+
{
294+
return false;
295+
}
296+
}
235297
}
236298
}

0 commit comments

Comments
 (0)