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

InteractiveBrowser LoginHint #21082

Merged
merged 2 commits into from
May 20, 2021
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
3 changes: 3 additions & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 1.5.0-beta.1 (Unreleased)

### Fixes and improvements

- Added `LoginHint` property to `InteractiveBrowserCredentialOptions` which allows a user name to be pre-selected for interactive logins. Setting this option skips the account selection prompt and immediately attempts to login with the specified account.

## 1.4.0 (2021-05-12)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ public InteractiveBrowserCredentialOptions() { }
public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } }
public string ClientId { get { throw null; } set { } }
public bool DisableAutomaticAuthentication { get { throw null; } set { } }
public string LoginHint { get { throw null; } set { } }
schaabs marked this conversation as resolved.
Show resolved Hide resolved
public System.Uri RedirectUri { get { throw null; } set { } }
public string TenantId { get { throw null; } set { } }
public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } }
Expand Down
47 changes: 32 additions & 15 deletions sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ namespace Azure.Identity
public class InteractiveBrowserCredential : TokenCredential
{
internal string ClientId { get; }
internal MsalPublicClient Client {get;}
internal string LoginHint { get; }
internal MsalPublicClient Client { get; }
internal CredentialPipeline Pipeline { get; }
internal bool DisableAutomaticAuthentication { get; }
internal AuthenticationRecord Record { get; private set; }

private const string AuthenticationRequiredMessage = "Interactive authentication is needed to acquire token. Call Authenticate to interactively authenticate.";

private const string NoDefaultScopeMessage = "Authenticating in this environment requires specifying a TokenRequestContext.";

/// <summary>
/// Creates a new <see cref="InteractiveBrowserCredential"/> with the specified options, which will authenticate users.
/// </summary>
public InteractiveBrowserCredential()
: this(null, Constants.DeveloperSignOnClientId, null, null)
{
}
{ }

/// <summary>
/// Creates a new <see cref="InteractiveBrowserCredential"/> with the specified options, which will authenticate users with the specified application.
Expand All @@ -52,8 +53,7 @@ public InteractiveBrowserCredential(InteractiveBrowserCredentialOptions options)
[EditorBrowsable(EditorBrowsableState.Never)]
public InteractiveBrowserCredential(string clientId)
: this(null, clientId, null, null)
{
}
{ }

/// <summary>
/// Creates a new <see cref="InteractiveBrowserCredential"/> with the specified options, which will authenticate users with the specified application.
Expand All @@ -64,23 +64,21 @@ public InteractiveBrowserCredential(string clientId)
/// <param name="options">The client options for the newly created <see cref="InteractiveBrowserCredential"/>.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public InteractiveBrowserCredential(string tenantId, string clientId, TokenCredentialOptions options = default)
: this(Validations.ValidateTenantId(tenantId, nameof(tenantId), allowNull:true), clientId, options, null, null)
{
}
: this(Validations.ValidateTenantId(tenantId, nameof(tenantId), allowNull: true), clientId, options, null, null)
{ }

internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCredentialOptions options, CredentialPipeline pipeline)
: this(tenantId, clientId, options, pipeline, null)
{
}
{ }

internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCredentialOptions options, CredentialPipeline pipeline, MsalPublicClient client)
{
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
Argument.AssertNotNull(clientId, nameof(clientId));

ClientId = clientId;
Pipeline = pipeline ?? CredentialPipeline.GetInstance(options);

LoginHint = (options as InteractiveBrowserCredentialOptions)?.LoginHint;
var redirectUrl = (options as InteractiveBrowserCredentialOptions)?.RedirectUri?.AbsoluteUri ?? Constants.DefaultRedirectUrl;

Client = client ?? new MsalPublicClient(Pipeline, tenantId, clientId, redirectUrl, options as ITokenCacheOptions);
}

Expand Down Expand Up @@ -182,7 +180,13 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
{
try
{
AuthenticationResult result = await Client.AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, Record, async, cancellationToken).ConfigureAwait(false);
AuthenticationResult result = await Client.AcquireTokenSilentAsync(
requestContext.Scopes,
requestContext.Claims,
Record,
async,
cancellationToken)
.ConfigureAwait(false);

return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn));
}
Expand All @@ -207,7 +211,20 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC

private async Task<AccessToken> GetTokenViaBrowserLoginAsync(TokenRequestContext context, bool async, CancellationToken cancellationToken)
{
AuthenticationResult result = await Client.AcquireTokenInteractiveAsync(context.Scopes, context.Claims, Prompt.SelectAccount, async, cancellationToken).ConfigureAwait(false);
Prompt prompt = LoginHint switch
{
null => Prompt.SelectAccount,
_ => Prompt.NoPrompt
};

AuthenticationResult result = await Client.AcquireTokenInteractiveAsync(
context.Scopes,
context.Claims,
prompt,
LoginHint,
async,
cancellationToken)
.ConfigureAwait(false);

Record = new AuthenticationRecord(result, ClientId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@ public string TenantId
/// The <see cref="Identity.AuthenticationRecord"/> captured from a previous authentication.
/// </summary>
public AuthenticationRecord AuthenticationRecord { get; set; }

/// <summary>
/// Avoids the account prompt and pre-populates the username of the account to login.
/// </summary>
public string LoginHint { get; set; }
}
}
27 changes: 18 additions & 9 deletions sdk/identity/Azure.Identity/src/MsalPublicClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ protected virtual async ValueTask<AuthenticationResult> AcquireTokenSilentCoreAs
.ConfigureAwait(false);
}

public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string[] scopes, string claims, Prompt prompt, bool async, CancellationToken cancellationToken)
public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string[] scopes, string claims, Prompt prompt, string loginHint, bool async, CancellationToken cancellationToken)
{
#pragma warning disable AZC0109 // Misuse of 'async' parameter.
if (!async && !IdentityCompatSwitches.DisableInteractiveBrowserThreadpoolExecution)
Expand All @@ -114,24 +114,33 @@ public async ValueTask<AuthenticationResult> AcquireTokenInteractiveAsync(string
AzureIdentityEventSource.Singleton.InteractiveAuthenticationExecutingOnThreadPool();

#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult().
return Task.Run(async () => await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, true, cancellationToken).ConfigureAwait(false)).GetAwaiter().GetResult();
return Task.Run(async () => await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, true, cancellationToken).ConfigureAwait(false)).GetAwaiter().GetResult();
#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult().
}

AzureIdentityEventSource.Singleton.InteractiveAuthenticationExecutingInline();

return await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, async, cancellationToken).ConfigureAwait(false);
return await AcquireTokenInteractiveCoreAsync(scopes, claims, prompt, loginHint, async, cancellationToken).ConfigureAwait(false);
}

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

return await client.AcquireTokenInteractive(scopes)
.WithPrompt(prompt)
.WithClaims(claims)
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false);
return loginHint switch
{
null => await client.AcquireTokenInteractive(scopes)
.WithPrompt(prompt)
.WithClaims(claims)
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false),
_ => await client.AcquireTokenInteractive(scopes)
.WithPrompt(prompt)
.WithClaims(claims)
.WithLoginHint(loginHint)
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false)
};
}

public async ValueTask<AuthenticationResult> AcquireTokenByUsernamePasswordAsync(string[] scopes, string claims, string username, SecureString password, bool async, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task ValidateDeviceCodeCredentialSucceededEvents()
[Test]
public async Task ValidateInteractiveBrowserCredentialSucceededEvents()
{
var mockMsalClient = new MockMsalPublicClient() { InteractiveAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(10)); } };
var mockMsalClient = new MockMsalPublicClient() { AuthFactory = _ => { return AuthenticationResultFactory.Create(accessToken: Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(10)); } };

var credential = InstrumentClient(new InteractiveBrowserCredential(default, Guid.NewGuid().ToString(), default, default, mockMsalClient));

Expand Down Expand Up @@ -159,7 +159,7 @@ public async Task ValidateInteractiveBrowserCredentialFailedEvents()
{
var expExMessage = Guid.NewGuid().ToString();

var mockMsalClient = new MockMsalPublicClient() { InteractiveAuthFactory = (_) => throw new MockClientException(expExMessage) };
var mockMsalClient = new MockMsalPublicClient() { AuthFactory = _ => throw new MockClientException(expExMessage) };

var credential = InstrumentClient(new InteractiveBrowserCredential(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), default, default, mockMsalClient));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task InteractiveBrowserAcquireTokenInteractiveException()
{
string expInnerExMessage = Guid.NewGuid().ToString();

var mockMsalClient = new MockMsalPublicClient() { InteractiveAuthFactory = (_) => { throw new MockClientException(expInnerExMessage); } };
var mockMsalClient = new MockMsalPublicClient { AuthFactory = _ => { throw new MockClientException(expInnerExMessage); } };

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

Expand All @@ -47,8 +47,8 @@ public async Task InteractiveBrowserAcquireTokenSilentException()

var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: expToken, expiresOn: expExpiresOn); },
SilentAuthFactory = (_) => { throw new MockClientException(expInnerExMessage); }
AuthFactory = _ => { return AuthenticationResultFactory.Create(expToken, expiresOn: expExpiresOn); },
SilentAuthFactory = _ => { throw new MockClientException(expInnerExMessage); }
};

var credential = InstrumentClient(new InteractiveBrowserCredential(default, "", default, default, mockMsalClient));
Expand Down Expand Up @@ -79,7 +79,7 @@ public async Task InteractiveBrowserRefreshException()

var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: expToken, expiresOn: expExpiresOn); },
AuthFactory = (_) => { return AuthenticationResultFactory.Create(expToken, expiresOn: expExpiresOn); },
SilentAuthFactory = (_) => { throw new MsalUiRequiredException("errorCode", "message"); }
};

Expand All @@ -91,7 +91,7 @@ public async Task InteractiveBrowserRefreshException()

Assert.AreEqual(expExpiresOn, token.ExpiresOn);

mockMsalClient.InteractiveAuthFactory = (_) => { throw new MockClientException(expInnerExMessage); };
mockMsalClient.AuthFactory = (_) => { throw new MockClientException(expInnerExMessage); };

var ex = Assert.ThrowsAsync<AuthenticationFailedException>(async () => await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)));

Expand Down Expand Up @@ -143,6 +143,24 @@ public async Task InteractiveBrowserValidateSyncWorkaroundCompatSwitch()
await ValidateSyncWorkaroundCompatSwitch(!IsAsync);
}

[Test]
public async Task LoginHint([Values(null, "fring@contoso.com")] string loginHint)
{
var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_, _, prompt, hintArg, _, _) =>
{
Assert.AreEqual(loginHint == null ? Prompt.SelectAccount : Prompt.NoPrompt, prompt);
Assert.AreEqual(loginHint, hintArg);
return AuthenticationResultFactory.Create(Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(5));
}
};
var options = new InteractiveBrowserCredentialOptions { LoginHint = loginHint };
var credential = InstrumentClient(new InteractiveBrowserCredential(default, "", options, default, mockMsalClient));

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

private async Task ValidateSyncWorkaroundCompatSwitch(bool expectedThreadPoolExecution)
{
bool threadPoolExec = false;
Expand All @@ -162,7 +180,7 @@ private async Task ValidateSyncWorkaroundCompatSwitch(bool expectedThreadPoolExe

var mockMsalClient = new MockMsalPublicClient
{
InteractiveAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(5)); }
AuthFactory = _ => { return AuthenticationResultFactory.Create(Guid.NewGuid().ToString(), expiresOn: DateTimeOffset.UtcNow.AddMinutes(5)); }
};

var credential = InstrumentClient(new InteractiveBrowserCredential(default, "", default, default, mockMsalClient));
Expand Down
15 changes: 10 additions & 5 deletions sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class MockMsalPublicClient : MsalPublicClient

public Func<string[], AuthenticationResult> UserPassAuthFactory { get; set; }

public Func<string[], AuthenticationResult> InteractiveAuthFactory { get; set; }
public Func<string[], string, Prompt, string, bool, CancellationToken, AuthenticationResult> InteractiveAuthFactory { get; set; }

public Func<string[], AuthenticationResult> SilentAuthFactory { get; set; }

Expand All @@ -45,13 +45,18 @@ protected override ValueTask<AuthenticationResult> AcquireTokenByUsernamePasswor
throw new NotImplementedException();
}

protected override ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAsync(string[] scopes, string claims, Prompt prompt, bool async, CancellationToken cancellationToken)
protected override ValueTask<AuthenticationResult> AcquireTokenInteractiveCoreAsync(string[] scopes, string claims, Prompt prompt, string loginHint, bool async, CancellationToken cancellationToken)
{
Func<string[], AuthenticationResult> factory = InteractiveAuthFactory ?? AuthFactory;
var interactiveAuthFactory = InteractiveAuthFactory;
var authFactory = AuthFactory;

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

throw new NotImplementedException();
Expand Down