Skip to content

Add a broker-free Entra sign-in path: ActiveDirectoryDefault returns before the WAM broker is ever built, so it is reachable where Entra MFA is not #3214

Description

@erikdarlingdata

#3196's reporter cannot sign in with Microsoft Entra MFA and there is nothing they can do about it from the app's side. #3201 made the failure diagnosable; it did not give them a way in. This is the way in.

Why Entra MFA cannot be repaired in place

Read from Microsoft's published source (dotnet/SqlClient, src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs at main), not from a decompiler:

private const string s_sqlClientApplicationId = "2fd908ad-0664-4344-b9be-cd3e8b574c38";   // :38
private readonly string _applicationClientId = s_sqlClientApplicationId;                  // :58
_useWamBroker = _applicationClientId == s_sqlClientApplicationId || options.UseWamBroker; // :112

The maintainers' own comment on the field: "Always true for the SqlClient first-party app id; for caller-supplied app ids, opt-in via the Options-pattern constructor." EntraInteractiveAuth calls the parameterless constructor, so the app id is the driver's own, the == arm is unconditionally true, and UseWamBroker cannot lower it. The Windows Account Manager is mandatory on that path, MSAL falls back to a browser only when the broker is unavailable rather than when an invoked broker refuses, and the refusal reason is redacted to Context: (pii) with no seam to enable it.

So the knob exists and is unreachable without shipping our own Entra app registration — a product decision with tenant-consent implications, not a patch.

ActiveDirectoryDefault never reaches the broker, and it is provable by control flow

In the same file:

  • :266-273 — the ActiveDirectoryDefault arm resolves a DefaultAzureCredential and returns at :272.
  • :334 — GetPublicClientAppInstanceAsync, the only path to the MSAL public-client app.
  • :907 — builder.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows)), reached only from that app's construction.

272 is before 334. The Default arm returns before any public-client app exists, so no broker is configured, invoked, or consulted. This is not an inference about MSAL's behaviour — it is the order of two statements in one method.

DefaultAzureCredential chains credential sources, and ExcludeAzureCliCredential defaults to false, so a developer or DBA who has run az login already has a working path. It also picks up environment, workload-identity and managed-identity credentials without an interactive prompt.

What exists today, and what does not

PerformanceMonitor.Common/Models/AuthenticationTypes.cs offers exactly five: Windows, SqlServer, EntraMFA, ServicePrincipal, ManagedIdentity. ActiveDirectoryDefault and ActiveDirectoryDeviceCodeFlow appear nowhere in the repository — zero references in any .cs or .xaml. So this is a new option, not a repair of an existing one.

ServicePrincipal is already broker-free and is what #3196's reply offers as today's workaround, but it needs someone who can register an application in the tenant and grant it database access. That is a real barrier for a single user evaluating the tool, which is the population #3196 is drawn from.

Scope

Add a broker-free interactive-or-ambient Entra option. ActiveDirectoryDefault is the one this issue argues for, on the strength of the control-flow proof above.

One claim deliberately NOT made: ActiveDirectoryDeviceCodeFlow was also suggested as broker-free. Its arm at :403 sits after the public-client app is built at :334, so the app it uses may well have WithBroker configured — the device-code grant not invoking WAM is a claim about MSAL's behaviour that this issue does not establish. If device code flow is wanted, prove that separately rather than inheriting Default's proof.

The verification limit, stated up front

This cannot be verified from here. It needs a real Entra tenant and a Windows host. #2184's fix was verified against a live tenant by the reporter of #2184, and #3196's reporter is the same route for this one. So the deliverable is a change whose doc states plainly that it is unverified against a live tenant, plus something the reporter can actually run — not a fix asserted to work.

Two-SKU parity applies: Darling's viewer rejects EntraMFA, ServicePrincipal and ManagedIdentity outright via ServerStoreCredential.UnsupportedAuthMessage, so establish whether a new mode belongs there before assuming it does.

Activity

  1. erikdarlingdata commented on Sep 9, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Three corrections to this issue as I filed it. The central claim survives; two of the three change what the reporter must eventually be told.

    1. I verified the wrong population, and so did the check I described as authoritative. This issue quotes ActiveDirectoryAuthenticationProvider.cs from dotnet/SqlClient at main. The repository pins Microsoft.Data.SqlClient 7.0.2. Reading main for a claim about a pinned version is exactly the defect this queue has spent the day naming — a figure measured against a population that does not govern.

    Re-read at tag v7.0.2: :38 is the app-id constant, :63 initialises _useWamBroker false, :112 is _useWamBroker = _applicationClientId == s_sqlClientApplicationId || options.UseWamBroker. Byte-identical to what is quoted here. So the conclusion holds — but it holds because that file happened not to move, not because the method was sound. Cite the tag, not the branch.

    2. "Broker-free" is a property of the DEPENDENCY GRAPH, not of the code — and this is a condition on the answer. Azure.Identity 1.18.0 places a BrokerCredential at the tail of DefaultAzureCredential's chain, and ExcludeBrokerCredential defaults to false. The chain is broker-free today only because Azure.Identity.Broker is absent: measured, zero occurrences across every lock file, Directory.Packages.props, and every project file in the repository.

    Worse than a pinned dependency: Azure.Identity is not pinned at all. It resolves transitively at 1.18.0 in all seven lock files that carry it, arriving through Microsoft.Data.SqlClient.Extensions.Azure's version range. So anyone adding Azure.Identity.Broker — directly or transitively, in any project that reaches Lite — silently reinstates the Windows broker at the end of the very path this issue exists to provide.

    That belongs in whatever #3196's reporter is eventually told. Telling an outside person "this route does not use the Web Account Manager" is a promise about our dependency graph, and it should be stated as one.

    3. The mode is AMBIENT ONLY, not "interactive-or-ambient" as this issue's scope says. ExcludeInteractiveBrowserCredential is hard-coded true in the driver, so there is no interactive arm to fall back to. The staged reporter reply already says the option will never show a sign-in prompt, so the text is correct — but it is correct about a constraint this issue mis-scoped, and the reply is currently the only place a reader learns it.

    The mis-scoping had a real cost: "interactive-or-ambient" is what makes a broker-free mode sound like a replacement for Entra MFA. It is not. It is a way to reuse a sign-in made elsewhere, and a user without one gets a failure rather than a prompt.

  2. erikdarlingdata commented on Sep 9, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Implemented by #3218, merged to dev as 878d091450de. Leaving this issue open until the reporter of #3196 has a build and has reported back — the code exists; whether it works on a live tenant is theirs to confirm, and closing on merge would record a verification nobody performed.

    Every line number in this issue is wrong, and it is the same error twice. I cited :272 (Default arm returns), :334 (public-client app built) and :907 (WithBroker) — those are main's. At the pinned v7.0.2 they are :264, :325 and :826. I already corrected the branch-versus-tag method above; the numbers it produced were left standing, which is the more quotable half.

    The ordering argument survives unchanged: 264 < 325. The Default arm still returns before any public-client app exists, so no broker is configured, invoked or consulted. _useWamBroker at :112 is identical between the two. But cite the tag's numbers, not the branch's.

    What #3218 established that this issue did not

    The control-flow proof is necessary and not sufficient, and this issue does not notice the gap. Azure.Identity 1.18.0 places a BrokerCredential at the tail of DefaultAzureCredential's chain, and SqlClient sets only ExcludeInteractiveBrowserCredential — so the chain genuinely contains a broker link. It is inert solely because Azure.Identity.Broker is absent from our dependency graph. Returning before the public-client app is built does not prevent a broker at the far end of a credential chain, and the proof in this issue would have read as complete. #3219 owns the dependency question; two pins in #3218 guard it, one using the identical Type.GetType call Azure.Identity's own CredentialOptionsMapper.GetBrokerOptions makes, so the pin cannot drift from the mechanism it models.

    And the mode is ambient only — ExcludeInteractiveBrowserCredential = true is hard-coded at :869. My "interactive-or-ambient" scoping is corrected above; what follows from it is that the failure message is the deliverable, not a detail, because a user with no ambient sign-in gets a failure rather than a prompt.

    Worth recording from the implementation

    Both broker-absence pins passed vacuously. They ran in a closure with no Azure.* assemblies on disk, so they asserted nothing — and would have stayed green with the broker package added, which is the exact condition they exist to detect. Found by building a real stand-in Azure.Identity.Broker assembly and watching them fail. A pin that models a dependency's absence has to be falsified by that dependency's presence, or it is testing its own environment.

    A classifier's "arms ordered by specificity" claim was not observable at all — the test passed with the arms swapped. Replaced with a two-direction depth assertion that a flattening walk fails.

    Darling rejects the mode by construction rather than by omission, established rather than assumed: MonitoredServerConnection builds only integrated or SQL-login connections and acquires no tokens, and MapAuth is a whitelist ending _ => null, so the mode was already refused on arrival across all three write paths with no edit to that file. No viewer display arm was added, because it would be dead code.

  3. erikdarlingdata commented on Sep 9, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Closing. Implemented and merged in #3218 as 878d091450de: AuthenticationTypes.EntraDefaultCredential, labelled Azure — Existing Sign-In (az login), mapping to SqlAuthenticationMethod.ActiveDirectoryDefault at Lite/Models/ServerConnection.cs:561.

    I had been holding this open as a placeholder for #3196's reporter to confirm on a live tenant. That is a waiting-on-someone state, not work, and #3196 is already the tracking item for it — keeping a second issue open for the same wait made the queue read as if there were two things to do.

    The corrections on this issue stand and are worth reading before anyone quotes it: every line number in the body is main's rather than v7.0.2's (:264/:325/:826, not :272/:334/:907); the control-flow proof is necessary but not sufficient, because Azure.Identity puts a BrokerCredential at the tail of the chain and returning before the public-client app is built does not prevent that; and the mode is ambient only, not "interactive-or-ambient" as the scope said — ExcludeInteractiveBrowserCredential is hard-coded true, so a user with no ambient sign-in gets a failure rather than a prompt.

    Whether it works against a live Entra tenant remains unverified by anyone here, and the reply to #3196's reporter is held behind six conditions until a build carries it and the version can be checked against the artifact rather than a .csproj.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions