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

Enable customerizing webview for interactive authentication #34095

Merged
merged 5 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.10.0-beta.2 (Unreleased)

### Features Added
- Add `BrowserCustomizedOptions` to `InteractiveBrowserCredential` to enable web view customization for interactive authentication.

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ public AzurePowerShellCredentialOptions() { }
public System.TimeSpan? ProcessTimeout { get { throw null; } set { } }
public string TenantId { get { throw null; } set { } }
}
public partial class BrowserCustomizationOptions
{
public bool? UseEmbeddedWebView;
public BrowserCustomizationOptions() { }
public string HtmlMessageError { get { throw null; } set { } }
public string HtmlMessageSuccess { get { throw null; } set { } }
}
public partial class ChainedTokenCredential : Azure.Core.TokenCredential
{
public ChainedTokenCredential(params Azure.Core.TokenCredential[] sources) { }
Expand Down Expand Up @@ -274,6 +281,7 @@ public partial class InteractiveBrowserCredentialOptions : Azure.Identity.TokenC
public InteractiveBrowserCredentialOptions() { }
public System.Collections.Generic.IList<string> AdditionallyAllowedTenants { get { throw null; } }
public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } }
public Azure.Identity.BrowserCustomizationOptions BrowserCustomizedOptions { get { throw null; } set { } }
public string ClientId { get { throw null; } set { } }
public bool DisableAutomaticAuthentication { get { throw null; } set { } }
public bool DisableInstanceDiscovery { get { throw null; } set { } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;

using Microsoft.Identity.Client;

namespace Azure.Identity
{
/// <summary>
/// Options to customize browser view.
/// </summary>
public class BrowserCustomizationOptions
{
/// <summary>
/// Specifies if the public client application should used an embedded web browser
/// or the system default browser
/// </summary>
public bool? UseEmbeddedWebView;

internal SystemWebViewOptions SystemBrowserOptions;

private SystemWebViewOptions systemWebViewOptions
{
get
{
SystemBrowserOptions ??= new SystemWebViewOptions();
return SystemBrowserOptions;
}
}

/// <summary>
/// Property to set HtmlMessageSuccess of SystemWebViewOptions from MSAL,
/// which the browser will show to the user when the user finishes authenticating successfully.
/// </summary>
public string HtmlMessageSuccess
{
get
{
return systemWebViewOptions.HtmlMessageSuccess;
}

set
{
systemWebViewOptions.HtmlMessageSuccess = value;
}
}

/// <summary>
/// Property to set HtmlMessageError of SystemWebViewOptions from MSAL,
/// which the browser will show to the user when the user finishes authenticating, but an error occurred.
/// You can use a string format e.g. "An error has occurred: {0} details: {1}".
/// </summary>
public string HtmlMessageError
{
get
{
return systemWebViewOptions.HtmlMessageError;
}

set
{
systemWebViewOptions.HtmlMessageError = value;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class InteractiveBrowserCredential : TokenCredential
internal string[] AdditionallyAllowedTenantIds { get; }
internal string ClientId { get; }
internal string LoginHint { get; }
internal BrowserCustomizationOptions BrowserCustomizedOptions { get; }
internal MsalPublicClient Client { get; }
internal CredentialPipeline Pipeline { get; }
internal bool DisableAutomaticAuthentication { get; }
Expand Down Expand Up @@ -88,6 +89,7 @@ internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCre
Client = client ?? new MsalPublicClient(Pipeline, tenantId, clientId, redirectUrl, options);
AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds((options as ISupportsAdditionallyAllowedTenants)?.AdditionallyAllowedTenants);
Record = (options as InteractiveBrowserCredentialOptions)?.AuthenticationRecord;
BrowserCustomizedOptions = (options as InteractiveBrowserCredentialOptions)?.BrowserCustomizedOptions;
}

/// <summary>
Expand Down Expand Up @@ -232,7 +234,7 @@ private async Task<AccessToken> GetTokenViaBrowserLoginAsync(TokenRequestContext

var tenantId = TenantIdResolver.Resolve(TenantId ?? Record?.TenantId, context, AdditionallyAllowedTenantIds);
AuthenticationResult result = await Client
.AcquireTokenInteractiveAsync(context.Scopes, context.Claims, prompt, LoginHint, tenantId, context.IsCaeEnabled, async, cancellationToken)
.AcquireTokenInteractiveAsync(context.Scopes, context.Claims, prompt, LoginHint, tenantId, context.IsCaeEnabled, BrowserCustomizedOptions, async, cancellationToken)
.ConfigureAwait(false);

Record = new AuthenticationRecord(result, ClientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Identity.Client;

namespace Azure.Identity
{
Expand Down Expand Up @@ -64,5 +65,10 @@ public string TenantId

/// <inheritdoc/>
public bool DisableInstanceDiscovery { get; set; }

/// <summary>
/// The options for customizing the browser for interactive authentication.
/// </summary>
public BrowserCustomizationOptions BrowserCustomizedOptions { get; set; }
}
}
20 changes: 16 additions & 4 deletions sdk/identity/Azure.Identity/src/MsalPublicClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Identity.Client;

namespace Azure.Identity
Expand Down Expand Up @@ -123,7 +124,7 @@ protected virtual async ValueTask<AuthenticationResult> AcquireTokenSilentCoreAs
.ConfigureAwait(false);
}

public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string[] scopes, string claims, Prompt prompt, string loginHint, string tenantId, bool enableCae, bool async, CancellationToken cancellationToken)
public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string[] scopes, string claims, Prompt prompt, string loginHint, string tenantId, bool enableCae, BrowserCustomizationOptions browserOptions, bool async, CancellationToken cancellationToken)
{
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && !IdentityCompatSwitches.DisableInteractiveBrowserThreadpoolExecution)
{
Expand All @@ -137,7 +138,7 @@ public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string
#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult().
return Task.Run(async () =>
{
var result = await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, tenantId, enableCae, true, cancellationToken).ConfigureAwait(false);
var result = await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, tenantId, enableCae, browserOptions, true, cancellationToken).ConfigureAwait(false);
LogAccountDetails(result);
return result;
}).GetAwaiter().GetResult();
Expand All @@ -146,12 +147,12 @@ public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string

AzureIdentityEventSource.Singleton.InteractiveAuthenticationExecutingInline();

var result = await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, tenantId, enableCae, async, cancellationToken).ConfigureAwait(false);
var result = await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, tenantId, enableCae, browserOptions, async, cancellationToken).ConfigureAwait(false);
LogAccountDetails(result);
return result;
}

protected virtual async ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAsync(string[] scopes, string claims, Prompt prompt, string loginHint, string tenantId, bool enableCae, bool async, CancellationToken cancellationToken)
protected virtual async ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAsync(string[] scopes, string claims, Prompt prompt, string loginHint, string tenantId, bool enableCae, BrowserCustomizationOptions browserOptions, bool async, CancellationToken cancellationToken)
{
IPublicClientApplication client = await GetClientAsync(enableCae, async, cancellationToken).ConfigureAwait(false);

Expand All @@ -168,6 +169,17 @@ protected virtual async ValueTask<AuthenticationResult> AcquireTokenInteractiveC
{
builder.WithAuthority(AuthorityHost.AbsoluteUri, tenantId);
}
if (browserOptions != null)
{
if (browserOptions.UseEmbeddedWebView.HasValue)
{
builder.WithUseEmbeddedWebView(browserOptions.UseEmbeddedWebView.Value);
}
if (browserOptions.SystemBrowserOptions != null)
{
builder.WithSystemWebViewOptions(browserOptions.SystemBrowserOptions);
}
}
return await builder
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false);
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/Azure.Identity/tests/CredentialTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ public void TestSetup(TokenCredentialOptions options = null)
// Assert.AreEqual(tenantId, tId);
return publicResult;
};
mockPublicMsalClient.InteractiveAuthFactory = (_, _, _, _, tenant, _, _) =>
mockPublicMsalClient.InteractiveAuthFactory = (_, _, _, _, tenant, _, _, _) =>
{
Assert.AreEqual(expectedTenantId, tenant, "TenantId passed to msal should match");
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Azure.Core.TestFramework;
using Azure.Identity.Tests.Mock;
using Microsoft.Identity.Client;

using NUnit.Framework;

namespace Azure.Identity.Tests
Expand Down Expand Up @@ -178,7 +179,7 @@ public async Task LoginHint([Values(null, "[email protected]")] string loginHint
{
var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_, _, prompt, hintArg, _, _, _) =>
InteractiveAuthFactory = (_, _, prompt, hintArg, _, _, _, _) =>
{
Assert.AreEqual(loginHint == null ? Prompt.SelectAccount : Prompt.NoPrompt, prompt);
Assert.AreEqual(loginHint, hintArg);
Expand Down Expand Up @@ -297,5 +298,59 @@ public async Task InvokesBeforeBuildClientOnExtendedOptions()

Assert.True(beforeBuildClientInvoked);
}

[Test]
public async Task BrowserCustomizedOptionsHtmlMessage([Values(null, "<p> Login Successfully.</p>")] string htmlMessageSuccess, [Values(null, "<p> An error occured: {0}. Details {1}</p>")] string htmlMessageError)
{
var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_, _, _, _, _, _, browserOptions, _) =>
{
Assert.AreEqual(false, browserOptions.UseEmbeddedWebView);
Assert.AreEqual(htmlMessageSuccess, browserOptions.HtmlMessageSuccess);
Assert.AreEqual(htmlMessageError, browserOptions.HtmlMessageError);
return AuthenticationResultFactory.Create(Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(5));
}
};
var options = new InteractiveBrowserCredentialOptions()
{
BrowserCustomizedOptions = new BrowserCustomizationOptions()
{
UseEmbeddedWebView = false,
HtmlMessageSuccess = htmlMessageSuccess,
HtmlMessageError = htmlMessageError
}
};

var credential = InstrumentClient(new InteractiveBrowserCredential(default, "", options, default, mockMsalClient));

await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
}

[Test]
public async Task BrowserCustomizedUseEmbeddedWebView([Values(null, true, false)] bool useEmbeddedWebView, [Values(null, "<p> An error occured: {0}. Details {1}</p>")] string htmlMessageError)
{
var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_, _, _, _, _, _, browserOptions, _) =>
{
Assert.AreEqual(useEmbeddedWebView, browserOptions.UseEmbeddedWebView);
Assert.AreEqual(htmlMessageError, browserOptions.HtmlMessageError);
return AuthenticationResultFactory.Create(Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(5));
}
};
var options = new InteractiveBrowserCredentialOptions()
{
BrowserCustomizedOptions = new BrowserCustomizationOptions()
{
UseEmbeddedWebView = useEmbeddedWebView,
HtmlMessageError = htmlMessageError
}
};

var credential = InstrumentClient(new InteractiveBrowserCredential(default, "", options, default, mockMsalClient));

await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class MockMsalPublicClient : MsalPublicClient
public List<IAccount> Accounts { get; set; }
public Func<string[], string, AuthenticationResult> AuthFactory { get; set; }
public Func<string[], string, AuthenticationResult> UserPassAuthFactory { get; set; }
public Func<string[], string, Prompt, string, string, bool, CancellationToken, AuthenticationResult> InteractiveAuthFactory { get; set; }
public Func<string[], string, Prompt, string, string, bool, BrowserCustomizationOptions, CancellationToken, AuthenticationResult> InteractiveAuthFactory { get; set; }
public Func<string[], string, AuthenticationResult> SilentAuthFactory { get; set; }
public Func<string[], string, IAccount, string, bool, CancellationToken, AuthenticationResult> ExtendedSilentAuthFactory { get; set; }
public Func<DeviceCodeInfo, CancellationToken, AuthenticationResult> DeviceCodeAuthFactory { get; set; }
Expand All @@ -30,7 +30,7 @@ public MockMsalPublicClient(AuthenticationResult result)
{
AuthFactory = (_, _) => result;
UserPassAuthFactory = (_, _) => result;
InteractiveAuthFactory = (_, _, _, _, _, _, _) => result;
InteractiveAuthFactory = (_, _, _, _, _, _, _, _) => result;
SilentAuthFactory = (_, _) => result;
ExtendedSilentAuthFactory = (_, _, _, _, _, _) => result;
DeviceCodeAuthFactory = (_, _) => result;
Expand Down Expand Up @@ -72,6 +72,7 @@ protected override ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAs
string loginHint,
string tenantId,
bool enableCae,
BrowserCustomizationOptions browserOptions,
bool async,
CancellationToken cancellationToken)
{
Expand All @@ -80,7 +81,7 @@ protected override ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAs

if (interactiveAuthFactory != null)
{
return new ValueTask<AuthenticationResult>(interactiveAuthFactory(scopes, claims, prompt, loginHint, tenantId, async, cancellationToken));
return new ValueTask<AuthenticationResult>(interactiveAuthFactory(scopes, claims, prompt, loginHint, tenantId, async, browserOptions, cancellationToken));
}
if (authFactory != null)
{
Expand Down