Skip to content

Commit

Permalink
InteractiveBrowser LoginHint (Azure#21082)
Browse files Browse the repository at this point in the history
* InteractiveBrowser LoginHint
  • Loading branch information
christothes authored May 20, 2021
1 parent f01f5a3 commit 4064c94
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 37 deletions.
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 { } }
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

0 comments on commit 4064c94

Please sign in to comment.