Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3db6009
Enable WAM Broker support for Entra ID Auth modes
cheenamalhotra May 13, 2026
7a0a460
Add test connection project for validating WAM broker behavior on UI …
cheenamalhotra Jun 6, 2026
d23e4a6
Merge branch 'main' into dev/cheena/entra-wam-broker
cheenamalhotra Jun 6, 2026
bc5178a
Update redirectURI + minor changes
cheenamalhotra Jun 6, 2026
240e5ce
Merge branch 'dev/cheena/entra-wam-broker' of https://github.com/dotn…
cheenamalhotra Jun 6, 2026
bca2a62
Comments/cleanup
cheenamalhotra Jun 6, 2026
58083b6
Update config file
cheenamalhotra Jun 6, 2026
fd54502
Update Redirect URI for Unix
cheenamalhotra Jun 6, 2026
b657f32
Remove duplication
cheenamalhotra Jun 6, 2026
01529eb
Fix unix build
cheenamalhotra Jun 6, 2026
152b44e
Apply suggestions from code review
cheenamalhotra Jun 6, 2026
fff68be
Apply suggestions from code review
cheenamalhotra Jun 6, 2026
17564ca
Apply suggestions from code review
cheenamalhotra Jun 6, 2026
0d105e0
Update comment
cheenamalhotra Jun 6, 2026
97fce03
useWamBroker option and tests
cheenamalhotra Jun 9, 2026
de30171
Merge branch 'main' into dev/cheena/entra-wam-broker
cheenamalhotra Jun 9, 2026
84949bb
Update api and docs
cheenamalhotra Jun 9, 2026
82a2d4e
handle .net standard
cheenamalhotra Jun 10, 2026
f14c637
Remove specs
cheenamalhotra Jun 10, 2026
81312dc
Add collection, address docs
cheenamalhotra Jun 10, 2026
5683852
fix: allow AzureSqlConnector fallback build on non-Windows
Copilot Jun 10, 2026
3f91b03
Address feedback
cheenamalhotra Jun 17, 2026
d23b760
Address comments/more changes
cheenamalhotra Jun 17, 2026
449d289
Verify Sibling Assembly
cheenamalhotra Jun 17, 2026
90760e3
Deprecation note for future
cheenamalhotra Jun 17, 2026
69b3787
Support clearing user token cache to enable retesting token acquisiti…
cheenamalhotra Jun 17, 2026
b44ece5
More improvements, clear token cache properly + fix device code flow …
cheenamalhotra Jun 17, 2026
b82106d
Remove temp file
cheenamalhotra Jun 17, 2026
3064e77
Apply suggestions from code review
cheenamalhotra Jun 17, 2026
d436a77
Remove unwanted new ctors, fix errors
cheenamalhotra Jun 17, 2026
6ef039e
Merge branch 'dev/cheena/entra-wam-broker' of https://github.com/dotn…
cheenamalhotra Jun 17, 2026
8c89141
Address PR review: fix </param>, expand WamBroker tests, README note
cheenamalhotra Jun 17, 2026
ff4f373
More improvements + tests
cheenamalhotra Jun 17, 2026
da1929d
Last Updates
cheenamalhotra Jun 17, 2026
681b64f
Update MSAL as well
cheenamalhotra Jun 17, 2026
e628e2b
Potential fix for pull request finding
cheenamalhotra Jun 17, 2026
f8a7c96
Merge branch 'main' into dev/cheena/entra-wam-broker
cheenamalhotra Jun 17, 2026
7eac499
Removed unwanted changes
cheenamalhotra Jun 18, 2026
744af8d
Missing doc
cheenamalhotra Jun 18, 2026
c003f4a
Final changes
cheenamalhotra Jun 18, 2026
8a72de1
More updates, rename options type
cheenamalhotra Jun 18, 2026
ed46a31
Update provider instance initialization flow, other minor changes
cheenamalhotra Jun 18, 2026
b802a2e
Disable broker for AAD Password auth (not supported by MSAL unless us…
cheenamalhotra Jun 18, 2026
a63ac86
Revert "Disable broker for AAD Password auth (not supported by MSAL u…
cheenamalhotra Jun 19, 2026
5fdca68
Remove AAD Password auth tests
cheenamalhotra Jun 19, 2026
9e703ed
Fix wording
cheenamalhotra Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@

<ItemGroup>
<PackageVersion Include="Azure.Identity" Version="1.18.0" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.83.0" />
Comment thread
mdaigle marked this conversation as resolved.
Outdated
</ItemGroup>

<!-- ===================================================================== -->
Expand Down
131 changes: 131 additions & 0 deletions specs/002-wam-broker/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Feature Specification: WAM Broker Support for Entra ID Authentication

**Feature Branch**: `dev/automation/wam-broker-support`
**Created**: 2026-05-20
**Status**: Draft
**References**:

- PR [#2884](https://github.com/dotnet/SqlClient/pull/2884) (original POC, closed)
- PR [#3874](https://github.com/dotnet/SqlClient/pull/3874) (updated POC, closed)
- ICM 781210079 (Authentication failure on persistent AVD with Conditional Access)

## Problem Statement

Microsoft.Data.SqlClient's `ActiveDirectoryIntegrated` and other Public Client Application (PCA) authentication flows do not pass device information when acquiring tokens. This causes failures on persistent Azure Virtual Desktop (AVD) devices when Conditional Access Policies require device compliance or MFA based on device state.

### Root Cause

MSAL's `AcquireTokenByIntegratedWindowsAuth` does not pass device claims to the identity provider. The Windows Web Account Manager (WAM) broker passes device information (PRT, device compliance state) to Entra ID, satisfying Conditional Access policies.

### MSAL PCA Compliance

Microsoft identity platform requires first-party applications using Public Client Applications to use WAM broker on Windows for compliance. This ensures:

- Device-based Conditional Access policies work correctly
- Primary Refresh Token (PRT) is leveraged for SSO
- Device compliance state is included in token requests

## Design

### Target Location

The `ActiveDirectoryAuthenticationProvider` is in `src/Microsoft.Data.SqlClient.Extensions/Azure/src/`. This package targets `net462;netstandard2.0`.

### Platform Support Matrix

| Platform | WAM Broker | Fallback |
| ---------- | ----------- | ---------- |
| Windows (.NET Framework 4.6.2+) | ✅ Supported | IWA (legacy) |
| Windows (.NET 8.0+ via netstandard2.0) | ✅ Supported | System browser |
| Linux/macOS (.NET via netstandard2.0) | ❌ Not available | System browser / IWA |

### Authentication Modes Covered

| Mode | WAM Broker Behavior |
| ------ | ------------------- |
| `ActiveDirectoryInteractive` | Uses WAM for interactive token acquisition on Windows |
| `ActiveDirectoryIntegrated` | Uses WAM broker to pass device claims (solves CAP issues) |
| `ActiveDirectoryDeviceCodeFlow` | Uses WAM for device code flow on Windows |
| `ActiveDirectoryPassword` | Uses WAM for username/password flow on Windows |
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
| `ActiveDirectoryDefault` | No change (uses Azure.Identity DefaultAzureCredential) |
| `ActiveDirectoryManagedIdentity` | No change (server-side, no WAM needed) |
| `ActiveDirectoryServicePrincipal` | No change (confidential client, no WAM needed) |
| `ActiveDirectoryWorkloadIdentity` | No change (workload identity, no WAM needed) |

### Architecture Changes

1. **Make class `partial`**: Split `ActiveDirectoryAuthenticationProvider` into platform-specific files
2. **Add WAM broker**: Configure `BrokerOptions` on `PublicClientApplicationBuilder` on Windows
3. **Parent window handle**: Provide window handle for WAM dialog (required by WAM on Windows)
4. **Cross-platform `SetParentActivityOrWindow`**: Replace `#if NETFRAMEWORK`-only `SetIWin32WindowFunc` with cross-platform `Func<object>` API

Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
### New Public APIs

```csharp
public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider
{
// Cross-platform API to set the parent window/activity for WAM dialog
// On Windows: accepts IntPtr (window handle) or IWin32Window via Func<object>
// On Unix: no-op (WAM not available)
public void SetParentActivityOrWindow(Func<object> parentActivityOrWindowFunc);
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
}
```

### Dependencies

- **New**: `Microsoft.Identity.Client.Broker` (same version as `Microsoft.Identity.Client`: 4.83.0)
- Conditional on Windows platform at runtime (the package includes platform-specific native binaries)

### File Changes

| File | Change |
| ------ | -------- |
| `Directory.Packages.props` | Add `Microsoft.Identity.Client.Broker` version |
| `Azure.csproj` | Add package reference |
| `ActiveDirectoryAuthenticationProvider.cs` | Make partial, add broker logic |
| `ActiveDirectoryAuthenticationProvider.Windows.cs` (NEW) | Windows-specific: parent window detection |
| `Interop/Interop.GetConsoleWindow.cs` (NEW) | P/Invoke for kernel32 GetConsoleWindow |
| `Interop/Interop.GetAncestor.cs` (NEW) | P/Invoke for user32 GetAncestor |

### Conditional Compilation Strategy

Since the Extensions/Azure project targets `net462;netstandard2.0`, we cannot use `#if _WINDOWS` (that's for the main SqlClient project). Instead:

- Use **runtime OS detection** (`RuntimeInformation.IsOSPlatform(OSPlatform.Windows)`) for broker activation
- The `Microsoft.Identity.Client.Broker` package is always referenced but only invoked on Windows
- Platform-specific partial class files use `#if NETFRAMEWORK` for .NET Framework-only code paths

### Implementation Flow

```flowchart
AcquireTokenAsync
├── Non-PCA methods (Default, MSI, ServicePrincipal, Workload) → unchanged
└── PCA methods (Interactive, Integrated, Password, DeviceCodeFlow)
├── Build PublicClientApplication with BrokerOptions (Windows only)
├── Set ParentActivityOrWindow for WAM dialog
├── Try silent token acquisition
└── If silent fails:
├── Windows + Broker: WAM handles interactive/integrated flow
└── Non-Windows: Fallback to existing behavior (system browser, IWA)
```

## Testing

### Unit Tests

- Verify `SetParentActivityOrWindow` stores the function correctly
- Verify `SetParentActivityOrWindow` throws `ArgumentNullException` for null argument
- Verify `IsSupported` returns true for all expected auth methods

### Manual/Integration Tests (require SQL Server)

- `ActiveDirectoryInteractive` with WAM on Windows
- `ActiveDirectoryIntegrated` with WAM on Windows (validates device claims pass)
- Verify Unix/macOS falls back to non-broker behavior
- Verify CAP-protected Azure SQL MI access works from AVD

## Rollout

- WAM broker is **always enabled** on Windows when using PCA flows
- No opt-in connection string keyword needed (aligns with MSAL PCA compliance requirements)
- Existing `SetIWin32WindowFunc` remains as a backward-compatible API on .NET Framework, delegating to `SetParentActivityOrWindow`
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.InteropServices;

namespace Microsoft.Data.SqlClient;

public sealed partial class ActiveDirectoryAuthenticationProvider
Comment thread
paulmedynski marked this conversation as resolved.
{
/// <summary>
/// Gets the parent window handle to be used for interactive authentication prompts
/// via the Windows Account Manager (WAM) broker.
/// </summary>
/// <returns>
/// The parent window handle as an <see cref="IntPtr"/>, or <see cref="IntPtr.Zero"/> if
/// not running on Windows or no window handle is available.
/// </returns>
private IntPtr GetParentWindow()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return IntPtr.Zero;
}

// If the user has provided a custom parent activity/window function, use it.
if (_parentActivityOrWindowFunc is not null)
{
object parentWindow = _parentActivityOrWindowFunc();
Comment thread
paulmedynski marked this conversation as resolved.
if (parentWindow is IntPtr hwnd)
{
return hwnd;
}
Comment thread
cheenamalhotra marked this conversation as resolved.
}
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated

// Fall back to finding the console window, then getting its root owner.
IntPtr consoleHandle = Interop.Kernel32.GetConsoleWindow();
Comment thread
paulmedynski marked this conversation as resolved.
if (consoleHandle != IntPtr.Zero)
{
IntPtr rootOwner = Interop.User32.GetRootOwner(consoleHandle);
if (rootOwner != IntPtr.Zero)
{
return rootOwner;
}
return consoleHandle;
}

return IntPtr.Zero;
}

/// <summary>
/// Gets the parent activity or window object for the broker authentication flow.
/// On Windows, returns the window handle. On other platforms, returns <see cref="IntPtr.Zero"/>.
/// </summary>
private object GetBrokerParentWindow()
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
{
return GetParentWindow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Data.SqlClient.Internal;

namespace Microsoft.Data.SqlClient;

/// <include file='../doc/ActiveDirectoryAuthenticationProvider.xml' path='docs/members[@name="ActiveDirectoryAuthenticationProvider"]/ActiveDirectoryAuthenticationProvider/*'/>
public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider
public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider
{
/// <summary>
/// This is a static cache instance meant to hold instances of "PublicClientApplication" mapping to information available in PublicClientAppKey.
Expand Down Expand Up @@ -118,6 +120,24 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication)
public void SetIWin32WindowFunc(Func<System.Windows.Forms.IWin32Window> iWin32WindowFunc) => _iWin32WindowFunc = iWin32WindowFunc;
#endif

private Func<object>? _parentActivityOrWindowFunc = null;
Comment thread
cheenamalhotra marked this conversation as resolved.

/// <summary>
/// Sets a function to return the parent activity or window handle to be used for
/// WAM (Web Account Manager) broker authentication prompts.
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
/// </summary>
/// <param name="parentActivityOrWindowFunc">
/// A function that returns an <see cref="IntPtr"/> window handle on Windows.
/// </param>
/// <remarks>
/// On Windows, this handle is used to parent the WAM broker dialog.
/// If not set, the provider will attempt to automatically detect the console window handle.
/// </remarks>
public void SetParentActivityOrWindow(Func<object> parentActivityOrWindowFunc)
{
_parentActivityOrWindowFunc = parentActivityOrWindowFunc ?? throw new ArgumentNullException(nameof(parentActivityOrWindowFunc));
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
}
Comment thread
cheenamalhotra marked this conversation as resolved.

/// <include file='../doc/ActiveDirectoryAuthenticationProvider.xml' path='docs/members[@name="ActiveDirectoryAuthenticationProvider"]/AcquireTokenAsync/*'/>
public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenticationParameters parameters)
Comment thread
mdaigle marked this conversation as resolved.
{
Expand Down Expand Up @@ -724,9 +744,32 @@ private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publ
// tenant.
.WithAuthority(publicClientAppKey.Authority);

// Enable WAM broker on Windows for all supported authentication modes.
// The broker provides enhanced security by enabling device-based Conditional Access
// policies through the Windows Account Manager (WAM).
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
builder.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows));
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated

// Set the parent window handle for broker UI.
// On .NET Framework, prefer the IWin32WindowFunc if provided by the caller.
#if NETFRAMEWORK
Comment thread
mdaigle marked this conversation as resolved.
if (publicClientAppKey.IWin32WindowFunc is not null)
{
builder.WithParentActivityOrWindow(publicClientAppKey.IWin32WindowFunc);
}
else
{
builder.WithParentActivityOrWindow(GetBrokerParentWindow);
}
#else
builder.WithParentActivityOrWindow(GetBrokerParentWindow);
#endif
}
#if NETFRAMEWORK
if (publicClientAppKey.IWin32WindowFunc is not null)
else if (publicClientAppKey.IWin32WindowFunc is not null)
{
// Not on Windows (shouldn't happen for NETFRAMEWORK, but be defensive).
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
builder.WithParentActivityOrWindow(publicClientAppKey.IWin32WindowFunc);
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<!-- Explicitly depend on the same version of Microsoft.Identity.Client as SqlClient. -->
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="Microsoft.Identity.Client.Broker" />
Comment thread
cheenamalhotra marked this conversation as resolved.
</ItemGroup>

<!-- CodeGen Targets ================================================= -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.InteropServices;

namespace Microsoft.Data.SqlClient;

internal static partial class Interop
Comment thread
paulmedynski marked this conversation as resolved.
Comment thread
paulmedynski marked this conversation as resolved.
{
internal static partial class User32
Comment thread
paulmedynski marked this conversation as resolved.
{
private const uint GA_ROOTOWNER = 3;

/// <summary>
/// Retrieves the handle to the ancestor of the specified window.
/// </summary>
[DllImport("user32.dll")]
private static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags);

/// <summary>
/// Gets the root owner window of the specified window handle.
/// </summary>
internal static IntPtr GetRootOwner(IntPtr hwnd)
{
return GetAncestor(hwnd, GA_ROOTOWNER);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.InteropServices;

namespace Microsoft.Data.SqlClient;

internal static partial class Interop
Comment thread
paulmedynski marked this conversation as resolved.
{
internal static partial class Kernel32
{
/// <summary>
/// Retrieves the window handle used by the console associated with the calling process.
/// </summary>
[DllImport("kernel32.dll")]
internal static extern IntPtr GetConsoleWindow();
}
}
Comment thread
cheenamalhotra marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.Data.SqlClient.Extensions.Azure.Test;

public class WamBrokerTests
{
[Fact]
public void SetParentActivityOrWindow_NullArgument_ThrowsArgumentNullException()
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
{
var provider = new ActiveDirectoryAuthenticationProvider();
Assert.Throws<ArgumentNullException>("parentActivityOrWindowFunc",
() => provider.SetParentActivityOrWindow(null!));
}

[Fact]
public void SetParentActivityOrWindow_ValidFunc_DoesNotThrow()
{
var provider = new ActiveDirectoryAuthenticationProvider();
provider.SetParentActivityOrWindow(() => IntPtr.Zero);
}

[Fact]
public void SetParentActivityOrWindow_CanBeCalledMultipleTimes()
{
var provider = new ActiveDirectoryAuthenticationProvider();
provider.SetParentActivityOrWindow(() => IntPtr.Zero);
provider.SetParentActivityOrWindow(() => new IntPtr(12345));
Comment thread
cheenamalhotra marked this conversation as resolved.
Outdated
}
}
Loading