Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUERY] Azure.Identity 1.11.4: how to use WebView2 control? #44550

Closed
WolfgangHG opened this issue Jun 13, 2024 · 24 comments
Closed

[QUERY] Azure.Identity 1.11.4: how to use WebView2 control? #44550

WolfgangHG opened this issue Jun 13, 2024 · 24 comments
Assignees
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. customer-reported Issues that are reported by GitHub users external to the Azure organization. needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team question The issue doesn't require a change to the product in order to be resolved. Most issues start as that

Comments

@WolfgangHG
Copy link

Library name and version

Azure.Identity 1.11.4

Query/Question

I use the package Microsoft.Graph to access the Microsoft Graph API.

This is a problem that came up with the Azure.Identity update to 1.11.4 (which updated from "Microsoft.Identity.Client" 4.60.3 to 4.61.3): previously, it used "Microsoft.Web.WebView2" and the azure login screen was shown in a WebView2 embedded browser as a modal dialog.
Now, the default system browser opens as a separate application, which is less comfortable for users. Also, it just hangs when you close the browser tab.

I have no idea how to bring back the old behavior.

I create the GraphServiceClient this way (using classes from "Azure.Identity"):

InteractiveBrowserCredentialOptions options = new InteractiveBrowserCredentialOptions
{
  TenantId = myTenant,
  ClientId = myClientId,
  AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
  RedirectUri = new Uri("http://localhost")
};

InteractiveBrowserCredential interactiveCredential = new InteractiveBrowserCredential(options);

GraphServiceClient graphClient = new GraphServiceClient(interactiveCredential, new List<string>() { "myScope" });

I tried to force the WebView2 control:

BrowserCustomizationOptions customizationOptions = new BrowserCustomizationOptions();
customizationOptions.UseEmbeddedWebView = true;

InteractiveBrowserCredentialOptions options = new InteractiveBrowserCredentialOptions
{
  ...
  BrowserCustomization = customizationOptions,
};

But this code results in an exception:

InteractiveBrowserCredential authentication failed: To enable the embedded webview on Windows, reference Microsoft.Identity.Client.Desktop and call the extension method .WithWindowsEmbeddedBrowserSupport().

Adding the package "Microsoft.Identity.Client.Desktop" does not bring me further, as the extension method "PublicClientApplicationBuilder.WithWindowsEmbeddedBrowserSupport" requires an application builder, and I have not idea how to use this application builder with a GraphServiceClient and Azure.Identity. This probably happens internally?

Attached sample contains two small projects for Azure.Identity 1.11.3 and 1.11.4 - you just need an Azure tenant and a Application Client ID, the click the "Do something" button to show the login form.
The project for 1.11.4 also contains the commented "BrowserCustomization" code snippet which would result in an error.
AzureIdentityTest.zip

Environment

No response

@github-actions github-actions bot added Azure.Identity Client This issue points to a problem in the data-plane of the library. customer-reported Issues that are reported by GitHub users external to the Azure organization. needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team question The issue doesn't require a change to the product in order to be resolved. Most issues start as that labels Jun 13, 2024
Copy link

Thank you for your feedback. Tagging and routing to the team member best able to assist.

@christothes
Copy link
Member

Hi @WolfgangHG
Yes, it seems MSAL did make a change, which only affects apps targeting netx.x--windows - it is described in their changelog here

Would you mind trying to use our Broker package, which uses the Web Account Manager? I think it will work with the embedded browser option.

After adding the reference, you would change your options code to this:

 // get the window handle of the form
 IntPtr hwnd = this.Handle;

 InteractiveBrowserCredentialBrokerOptions options = new InteractiveBrowserCredentialBrokerOptions(parentWindowHandle: hwnd)
 {
     TenantId = this.textBoxTenant.Text,
     ClientId = this.textBoxClientID.Text,
     AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
     RedirectUri = new Uri("http://localhost"),
     BrowserCustomization = customizationOptions
 };

@christothes christothes added the needs-author-feedback Workflow: More information is needed from author to address the issue. label Jun 13, 2024
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Jun 13, 2024
Copy link

Hi @WolfgangHG. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

@WolfgangHG
Copy link
Author

@christothes I got it working after having added a new redirect URI in Azure (specifying "http://localhost" in InteractiveBrowserCredentialBrokerOptions does not seem to have an effect) :

AADSTS50011: The redirect URI 'ms-appx-web://Microsoft.AAD.BrokerPlugin/<my_app_id>' specified in the request does not match the redirect URIs configured for the application '<my_app_id>'. Make sure the redirect URI sent in the request matches one added to your application in the Azure portal. Navigate to https://aka.ms/redirectUriMismatchError to learn more about how to fix this.

Just for the records: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity.Broker/README.md#redirect-uris

This is bad, as we would have to tell all of our customers to add a new redirect uri to their azure configuration - and the url is specific for each customer.

The "BrowserCustomizationOptions.UseEmbeddedWebView" option does not seem to have any effect, it is always a login dialog. But it does not show the list of previously used logins (and probably does not allow selecting an already logged in user). This worked with Azure.Identity 1.11.3.
The login form does not seem to be really modal to my sample, though I set the window handle argument - at least while debugging. When switching to a different application while the login dialog is shown, I cannot click my app in taskbar to activate the login form again.

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed needs-author-feedback Workflow: More information is needed from author to address the issue. labels Jun 13, 2024
@christothes
Copy link
Member

Glad you got it working.

The "BrowserCustomizationOptions.UseEmbeddedWebView" option does not seem to have any effect, it is always a login dialog. But it does not show the list of previously used logins (and probably does not allow selecting an already logged in user). This worked with Azure.Identity 1.11.3. The login form does not seem to be really modal to my sample, though I set the window handle argument - at least while debugging. When switching to a different application while the login dialog is shown, I cannot click my app in taskbar to activate the login form again.

Yes, you are correct. In the case of the broker based login, the window is owned by the operating system. The window handle is only used to ensure it appears above the window related to the handle.

@christothes christothes added the issue-addressed Workflow: The Azure SDK team believes it to be addressed and ready to close. label Jun 13, 2024
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Jun 13, 2024
Copy link

Hi @WolfgangHG. Thank you for opening this issue and giving us the opportunity to assist. We believe that this has been addressed. If you feel that further discussion is needed, please add a comment with the text "/unresolve" to remove the "issue-addressed" label and continue the conversation.

@WolfgangHG
Copy link
Author

WolfgangHG commented Jun 14, 2024

@christothes Do you internally use a "PublicClientApplicationBuilder"? If yes: is there any chance to use the "PublicClientApplicationBuilder.WithWindowsEmbeddedBrowserSupport" extension method approach suggested by Microsoft.Identity.Client? I hope that this one causes less trouble than the Broker approach (change to the redirect url required, clunky modality)?

You set the status to "issue-addressed", but currently we have only found a workaround that I don't like ;-).

@WolfgangHG
Copy link
Author

/unresolve

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed issue-addressed Workflow: The Azure SDK team believes it to be addressed and ready to close. labels Jun 14, 2024
@christothes
Copy link
Member

@christothes Do you internally use a "PublicClientApplicationBuilder"? If yes: is there any chance to use the "PublicClientApplicationBuilder.WithWindowsEmbeddedBrowserSupport" extension method approach suggested by Microsoft.Identity.Client? I hope that this one causes less trouble than the Broker approach (change to the redirect url required, clunky modality)?

We don't intend to expose the functionality for WithWindowsEmbeddedBrowserSupport for the same reason this behavior changed in MSAL - it requires a dependency on Windows Forms to work properly, and we don't want everyone to require that.

@christothes christothes added the needs-author-feedback Workflow: More information is needed from author to address the issue. label Jun 14, 2024
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Jun 14, 2024
Copy link

Hi @WolfgangHG. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

@WolfgangHG
Copy link
Author

WolfgangHG commented Jun 16, 2024

@christothes I took a look at the code. It seems I cannot extend InteractiveBrowserCredential and call PublicClientApplicationBuilder.WithWindowsEmbeddedBrowserSupport myself. Or do you see a change to do so?

It might work if the interface Azure.Identity.IMsalPublicClientInitializerOptions was public and the BeforeBuildClient property would be a Func instead of an action. Then I could subclass InteractiveBrowserCredential and additionally implement this interface.

Attached is a diff of the necessary changes to Azure.Identity. What do you think?
diff.txt

With this code, I could create my own options subclass:

    public class MyMsalPublicClientInitializerOptions : InteractiveBrowserCredentialOptions, IMsalPublicClientInitializerOptions
    {
      Func<PublicClientApplicationBuilder, PublicClientApplicationBuilder> IMsalPublicClientInitializerOptions.BeforeBuildClient
      {
        get
        {
          return new Func<PublicClientApplicationBuilder, PublicClientApplicationBuilder>(builder => builder.WithWindowsEmbeddedBrowserSupport());
        }
      }

      bool IMsalPublicClientInitializerOptions.UseDefaultBrokerAccount { get; set; }
    }

But I assume the interface "IMsalPublicClientInitializerOptions" is more of an internal hack for some testing purposes, so I am not sure whether this is the proper place to extend Azure.Identity.

I would prefer to move "BeforeBuildClient" to "InteractiveBrowserCredentialOptions", so that "IMsalPublicClientInitializerOptions" could be kept internal.

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed needs-author-feedback Workflow: More information is needed from author to address the issue. labels Jun 16, 2024
@WolfgangHG
Copy link
Author

Another option could be to subclass Azure.Identity.MsalPublicClient (also not public) and enhance InteractiveBrowserCredential so that the Client can be specified in some constructor. But this seems to be a lot more code changes.

@christothes
Copy link
Member

christothes commented Jun 17, 2024

Hi @WolfgangHG -
We avoid exposing any MSAL types in our public API surface, so I'm afraid there is no way for us to expose this functionality. If you require full control of the underlying MSAL APIs, I'd recommend using Microsoft.Identity.Client directly in your application.

@WolfgangHG
Copy link
Author

@christothes This is unfortunately not possible I fear. I start from the Microsoft Graph .NET Client Library, and the https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/GraphServiceClient.cs has only constructors that accept TokenCredential or IAuthenticationProvider.
They suggest in their doc to use Azure.Identity.

Do you have a suggestion or a sample how to create a winforms login without Azure.Identity? I actually don't want to copy a lot of lines of code to call builder.WithWindowsEmbeddedBrowserSupport.

What about my suggestion to move the "BeforeBuildClient" property to "InteractiveBrowserCredentialOptions", so that "IMsalPublicClientInitializerOptions" could be kept internal? This does not expose MSAL classes I think.

@christothes
Copy link
Member

One option would be to create your own implementation of TokenCredential and in the implementation, use the PublicClientApplication, similar to how the InteractiveBrowserCredential does in Azure.Identity. See this example

@WolfgangHG
Copy link
Author

Well, I got it to work, but I don't like the solution:

string[] scopes = new string[] { "Calendars.Read" };

var app = PublicClientApplicationBuilder.Create(this.textBoxClientID.Text)
    .WithDefaultRedirectUri()
    .WithTenantId(this.textBoxTenant.Text)
    .WithWindowsEmbeddedBrowserSupport()
    .Build();

AuthenticationResult result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();

MyTokenCredential mtc = new MyTokenCredential(result);
GraphServiceClient graphClient = new GraphServiceClient(mtc);
         
Calendar cal = await graphClient.Me.Calendar.GetAsync();
MessageBox.Show(this, "Calendar ID: " + cal.Id);

Class "MyTokenCredential" is simple:

public class MyTokenCredential : TokenCredential
{
  AuthenticationResult result;
  public MyTokenCredential(AuthenticationResult result)
  {
    this.result = result;
  }
  public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    AccessToken token = new AccessToken(this.result.AccessToken, this.result.ExpiresOn);
    return new ValueTask<AccessToken>(token);
  }

  public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }
}

Could you comment on whether this is reasonable? Fortunately, our app does not need features like token refresh, as we use the interactive login only for short term operations and get a fresh token before each operation (at least I hope our usage works like this).

But with this code, we probably loose all the features of Azure.Identity.

@christothes
Copy link
Member

christothes commented Jun 18, 2024

Your MyTokenCredential implementation should be acquiring the token from the PublicClientApplication inside GetTokenAsync, and initializing the builder in the constructor. I wouldn't recommend just passing the AuthenticationResult to MyTokenCredential and having it use that static value.

I'd also recommend you acquire the token similar to how the example linked in my previous response does it so that you only get prompted for credentials the first time GetTokenAsync is called. MSAL will handle caching and token refresh automatically.

@WolfgangHG
Copy link
Author

You mean something like this?

In the form button click:

private MyTokenCredential mtc;

private async void buttonDoSomething_Click(object sender, EventArgs e)
{
  if (this.mtc == null)
  {
    mtc = new MyTokenCredential(this.textBoxTenant.Text, this.textBoxClientID.Text);
  }
  GraphServiceClient graphClient = new GraphServiceClient(mtc);

  Calendar cal = await graphClient.Me.Calendar.GetAsync();
  MessageBox.Show(this, "Calendar ID: " + cal.Id);
}

Class "MyTokenCredential" looks like this:

public class MyTokenCredential : TokenCredential
{
  private IPublicClientApplication clientApp;

  public MyTokenCredential(string _tenant, string _clientId)
  {
    this.clientApp = PublicClientApplicationBuilder.Create(_clientId)
          .WithDefaultRedirectUri()
          .WithTenantId(_tenant)
          .WithWindowsEmbeddedBrowserSupport()
          .Build();
  }
  public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    string[] scopes = new string[] { "Calendars.Read" };

    AuthenticationResult result;
    try
    {
      var accounts = await this.clientApp.GetAccountsAsync();
      result = await this.clientApp.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
        .ExecuteAsync();
    }
    catch (MsalUiRequiredException)
    {
      result = await this.clientApp.AcquireTokenInteractive(scopes).ExecuteAsync();
    }

    AccessToken token = new AccessToken(result.AccessToken, result.ExpiresOn);
    return token;
  }

  public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }
}

@WolfgangHG
Copy link
Author

@christothes May I send a little reminder about my latest workaround suggestion?

@christothes
Copy link
Member

Hi @WolfgangHG - yes, that looks better.

@WolfgangHG
Copy link
Author

WolfgangHG commented Jun 25, 2024

Thanks for the feedback.

Here is a slightly improved code snippet:
-as "GraphServiceClient" has a scopes parameter, I could use the "TokenRequestContext.Scopes"
-calling "WithParentActivityOrWindow" makes the login screen a perfect modal dialog.

In the form button click:

private MyTokenCredential mtc;

private async void buttonDoSomething_Click(object sender, EventArgs e)
{
  if (this.mtc == null)
  {
    mtc = new MyTokenCredential(this.textBoxTenant.Text, this.textBoxClientID.Text, this);
  }
  string[] scopes = new string[] { "Calendars.Read" };
  GraphServiceClient graphClient = new GraphServiceClient(mtc, scopes);

  Calendar cal = await graphClient.Me.Calendar.GetAsync();
  MessageBox.Show(this, "Calendar ID: " + cal.Id);
}

Class "MyTokenCredential" looks like this:

public class MyTokenCredential : TokenCredential
{
  private IPublicClientApplication clientApp;

  public MyTokenCredential(string _tenant, string _clientId, Form _formParent)
  {
    this.clientApp = PublicClientApplicationBuilder.Create(_clientId)
          .WithDefaultRedirectUri()
          .WithTenantId(_tenant)
          .WithWindowsEmbeddedBrowserSupport()
          .WithParentActivityOrWindow ( () => _formParent.Handle)
          .Build();
  }
  public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    AuthenticationResult result;
    try
    {
      var accounts = await this.clientApp.GetAccountsAsync();
      result = await this.clientApp.AcquireTokenSilent(requestContext.Scopes, accounts.FirstOrDefault())
        .ExecuteAsync();
    }
    catch (MsalUiRequiredException)
    {
      result = await this.clientApp.AcquireTokenInteractive(requestContext.Scopes).ExecuteAsync();
    }

    AccessToken token = new AccessToken(result.AccessToken, result.ExpiresOn);
    return token;
  }

  public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
  {
    throw new NotImplementedException();
  }
}

@WolfgangHG
Copy link
Author

@christothes I try one last time to convince you to add some callback that allows customization of the "IPublicClientApplication" that is generated internally in Azure.Identity ;-). Is there any chance that you add e.g. a "BeforeBuild" func to InteractiveBrowserCredentialOptions or InteractiveBrowserCredential so that I could create a "IPublicClientApplication" with "WithWindowsEmbeddedBrowserSupport"?
As I wrote before, my workaround works, but I loose all the smart code that you added in Azure.Identity and that I probably don't even have knowledge of.

If this is not possible, I suggest to add the findings of this ticket to some wiki page, as others might be affected by the fact that Azure.Identity does not use the embedded webview since 1.11.4. But Readme.md does not seem the proper place to do so. I found https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/tokencredentials.md in the GraphSDK project which has sample snippets.

@WolfgangHG
Copy link
Author

@christothes So there is nothing to be done about this in Azure.Identity? Shall I close this issue?

@christothes christothes closed this as not planned Won't fix, can't repro, duplicate, stale Jul 17, 2024
@christothes
Copy link
Member

Yes, unfortunately there is no change we can make in Azure.Identity for this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. customer-reported Issues that are reported by GitHub users external to the Azure organization. needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team question The issue doesn't require a change to the product in order to be resolved. Most issues start as that
Projects
Development

Successfully merging a pull request may close this issue.

2 participants