MSAL issue in LINQPad 8 - hangs on non success
I was experiencing an issue with the Util.MSAL.AcquireTokenAsync
function for a little bit. I'm not really sure what happened but what used to work would just start giving a big white box with the LINQPad-generated heading at the top with the checkbox for saves, but nothing in the webview2 control.
I had to play with it some, do some debugging with Claude and attach Visual Studio to LINQPad to figure out the issue.
In the file MSALCustomWebUI.cs
is this snippet (decompiled code so it probably doesn't actually match your real code):
webView.CoreWebView2.NavigationStarting += delegate(object? sender, CoreWebView2NavigationStartingEventArgs args) { if (args.Uri.Contains("?code=")) { if (chkSaveToken.Checked) { ClientAppWrapper.EnableCache(); } resultDriver.TrySetResult(new Uri(args.Uri)); args.Cancel = true; form.Close(); } };
This was just endlessly looping. The URI being returned by MSAL was this:
https://login.microsoftonline.com/common/oauth2/nativeclient?error=invalid_request&error_description=AADSTS65002%3a+Consent+between+first+party+application+%27a94f9c62-97fe-4d19-b06d-472bed8d2bcf%27+and+first+party+resource+%2700000003-0000-0000-c000-000000000000%27+must+be+configured+via+preauthorization+-+applications+owned+and+operated+by+Microsoft+must+get+approval+from+the+API+owner+before+requesting+tokens+for+that+API.+Trace+ID%3a+c2f606fc-8a5b-4b5a-a271-887c87543e00+Correlation+ID%3a+0ad352be-3104-4a83-808e-f8ef43cc3032+Timestamp%3a+2025-08-04+07%3a54%3a06Z&error_uri=https%3a%2f%2flogin.microsoftonline.com%2ferror%3fcode%3d65002&state=715d8268-4bbf-4762-817f-d25508ebf618275a785c-eb98-40ba-add4-f576fbb91db7
So it was trying to throw an error about the scopes, but that snippet above in LP8 just ignores it and keeps looping.
Using Claude as a helper I finally got it to work - I had to specifically request the scope "https://management.azure.com/.default" because I was trying to instantiate a Azure.ResourceManager.ArmClient
.
So now my #load
able query for Azure Credentials looks like this:
#load "GetCurrentUpn" public class LINQPadTokenCredential : TokenCredential { public readonly string Authority, UserIDHint; public readonly string[] Scopes; public readonly bool Refresh; public LINQPadTokenCredential(string authority, string userIDHint, bool refresh = false) : this(authority, userIDHint, refresh, []) { } public LINQPadTokenCredential (string authority, string userIDHint, bool refresh = false, params string[] scopes) => (Authority, UserIDHint, Scopes, Refresh) = (authority, userIDHint, scopes ?? Array.Empty<string>(), refresh); public override AccessToken GetToken (TokenRequestContext requestContext, CancellationToken cancelToken) => GetTokenAsync(requestContext, cancelToken).ConfigureAwait(false).GetAwaiter().GetResult(); public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancelToken) { Util.IAuthenticationToken auth; var promptValue = Refresh ? Util.MSAL.Prompt.ForceLogin : Util.MSAL.Prompt.NoPromptUnlessNecessary; if (Scopes.Length > 0) auth = await Util.MSAL.AcquireTokenAsync(Authority, Scopes, UserIDHint, prompt: promptValue).ConfigureAwait(false); else auth = await Util.MSAL.AcquireTokenAsync(Authority,UserIDHint, prompt: promptValue).ConfigureAwait(false); return new AccessToken(auth.AccessToken, auth.ExpiresOn); } } public const string TenantKey = "DefaultTenantId"; public string TenantId { get { var tenantId = Util.LoadString(TenantKey); if (string.IsNullOrEmpty(tenantId)) { return (TenantId = "<your tenant here - this is a hack>"); } return tenantId; } set { Util.SaveString(TenantKey, value); } } // And here's how to use it: async Task Main() { // Edit this if you're not using Azure Public Cloud string authEndPoint = Util.AzureCloud.PublicCloud.AuthenticationEndpoint; string userHint = GetCurrentUPN()!; // Authenticate to the Azure management API: var credential = new LINQPadTokenCredential(authEndPoint + TenantId, userHint, scopes: "https://management.azure.com/.default"); var client = new ArmClient(credential); // Dump the default subscription: var sub = await client.GetDefaultSubscriptionAsync(); sub.Data.Dump(1); }
But yeah, it looks like the MSAL thing can return errors and you might need more robust handling for that so you don't end up with a white box that does nothing and no clue why.
Haven't checked if this is fixed in 9 yet.
Best Answer
-
Thanks for the detailed info. I can see the problem: the query string in the error URI contains an "error_uri" key whose value includes the
?code
text. I'll make the URI validation more robust in LINQPad 9 so that it usesSystem.Web.HttpUtility.ParseQueryString
to parse the query string. Then it will activate only if there's a top-levelcode
key present.