Skip to content

Commit b0403de

Browse files
authored
Call AcquireTokenSilent after AuthenticationRecord has been fetched (Azure#23361)
* Call AcquireTokenSilent after AuthenticationRecord has been fetched * handle MsalUiRequiredException
1 parent f34705e commit b0403de

File tree

5 files changed

+112
-30
lines changed

5 files changed

+112
-30
lines changed

sdk/identity/Azure.Identity/src/AzureIdentityEventSource.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal sealed class AzureIdentityEventSource : AzureEventSource
3030
private const int DefaultAzureCredentialCredentialSelectedEvent = 13;
3131
private const int ProcessRunnerErrorEvent = 14;
3232
private const int ProcessRunnerInfoEvent = 15;
33+
private const int UsernamePasswordCredentialAcquireTokenSilentFailedEvent = 16;
3334

3435
private AzureIdentityEventSource() : base(EventSourceName) { }
3536

@@ -260,5 +261,23 @@ public void LogProcessRunnerInformational(string message)
260261
{
261262
WriteEvent(ProcessRunnerInfoEvent, message);
262263
}
264+
265+
[NonEvent]
266+
public void UsernamePasswordCredentialAcquireTokenSilentFailed(Exception e)
267+
{
268+
if (IsEnabled(EventLevel.Informational, EventKeywords.All))
269+
{
270+
UsernamePasswordCredentialAcquireTokenSilentFailed(FormatException(e));
271+
}
272+
}
273+
274+
[Event(
275+
UsernamePasswordCredentialAcquireTokenSilentFailedEvent,
276+
Level = EventLevel.Informational,
277+
Message = "UsernamePasswordCredential failed to acquire token silently. Error: {1}")]
278+
public void UsernamePasswordCredentialAcquireTokenSilentFailed(string error)
279+
{
280+
WriteEvent(UsernamePasswordCredentialAcquireTokenSilentFailedEvent, error);
281+
}
263282
}
264283
}

sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public class UsernamePasswordCredential : TokenCredential
3333
/// Protected constructor for mocking
3434
/// </summary>
3535
protected UsernamePasswordCredential()
36-
{
37-
}
36+
{ }
3837

3938
/// <summary>
4039
/// Creates an instance of the <see cref="UsernamePasswordCredential"/> with the details needed to authenticate against Azure Active Directory with a simple username
@@ -46,8 +45,7 @@ protected UsernamePasswordCredential()
4645
/// <param name="clientId">The client (application) ID of an App Registration in the tenant.</param>
4746
public UsernamePasswordCredential(string username, string password, string tenantId, string clientId)
4847
: this(username, password, tenantId, clientId, (TokenCredentialOptions)null)
49-
{
50-
}
48+
{ }
5149

5250
/// <summary>
5351
/// Creates an instance of the <see cref="UsernamePasswordCredential"/> with the details needed to authenticate against Azure Active Directory with a simple username
@@ -60,8 +58,7 @@ public UsernamePasswordCredential(string username, string password, string tenan
6058
/// <param name="options">The client options for the newly created UsernamePasswordCredential</param>
6159
public UsernamePasswordCredential(string username, string password, string tenantId, string clientId, TokenCredentialOptions options)
6260
: this(username, password, tenantId, clientId, options, null, null)
63-
{
64-
}
61+
{ }
6562

6663
/// <summary>
6764
/// Creates an instance of the <see cref="UsernamePasswordCredential"/> with the details needed to authenticate against Azure Active Directory with a simple username
@@ -74,10 +71,16 @@ public UsernamePasswordCredential(string username, string password, string tenan
7471
/// <param name="options">The client options for the newly created UsernamePasswordCredential</param>
7572
public UsernamePasswordCredential(string username, string password, string tenantId, string clientId, UsernamePasswordCredentialOptions options)
7673
: this(username, password, tenantId, clientId, options, null, null)
77-
{
78-
}
79-
80-
internal UsernamePasswordCredential(string username, string password, string tenantId, string clientId, TokenCredentialOptions options, CredentialPipeline pipeline, MsalPublicClient client)
74+
{ }
75+
76+
internal UsernamePasswordCredential(
77+
string username,
78+
string password,
79+
string tenantId,
80+
string clientId,
81+
TokenCredentialOptions options,
82+
CredentialPipeline pipeline,
83+
MsalPublicClient client)
8184
{
8285
Argument.AssertNotNull(username, nameof(username));
8386
Argument.AssertNotNull(password, nameof(password));
@@ -126,7 +129,8 @@ public virtual async Task<AuthenticationRecord> AuthenticateAsync(CancellationTo
126129
/// <returns>The <see cref="AuthenticationRecord"/> of the authenticated account.</returns>
127130
public virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
128131
{
129-
return AuthenticateImplAsync(false, requestContext, cancellationToken).EnsureCompleted();
132+
AuthenticateImplAsync(false, requestContext, cancellationToken).EnsureCompleted();
133+
return _record;
130134
}
131135

132136
/// <summary>
@@ -137,7 +141,8 @@ public virtual AuthenticationRecord Authenticate(TokenRequestContext requestCont
137141
/// <returns>The <see cref="AuthenticationRecord"/> of the authenticated account.</returns>
138142
public virtual async Task<AuthenticationRecord> AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
139143
{
140-
return await AuthenticateImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
144+
await AuthenticateImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
145+
return _record;
141146
}
142147

143148
/// <summary>
@@ -166,15 +171,19 @@ public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext r
166171
return await GetTokenImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false);
167172
}
168173

169-
private async Task<AuthenticationRecord> AuthenticateImplAsync(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
174+
private async Task<AuthenticationResult> AuthenticateImplAsync(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
170175
{
171176
using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope($"{nameof(UsernamePasswordCredential)}.{nameof(Authenticate)}", requestContext);
172-
173177
try
174178
{
175-
scope.Succeeded(await GetTokenImplAsync(async, requestContext, cancellationToken).ConfigureAwait(false));
179+
var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _allowMultiTenantAuthentication);
176180

177-
return _record;
181+
AuthenticationResult result = await Client
182+
.AcquireTokenByUsernamePasswordAsync(requestContext.Scopes, requestContext.Claims, _username, _password, tenantId, async, cancellationToken)
183+
.ConfigureAwait(false);
184+
185+
_record = new AuthenticationRecord(result, _clientId);
186+
return result;
178187
}
179188
catch (Exception e)
180189
{
@@ -185,17 +194,25 @@ private async Task<AuthenticationRecord> AuthenticateImplAsync(bool async, Token
185194
private async Task<AccessToken> GetTokenImplAsync(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
186195
{
187196
using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("UsernamePasswordCredential.GetToken", requestContext);
188-
189197
try
190198
{
191-
var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _allowMultiTenantAuthentication);
192-
193-
AuthenticationResult result = await Client
194-
.AcquireTokenByUsernamePasswordAsync(requestContext.Scopes, requestContext.Claims, _username, _password, tenantId, async, cancellationToken)
195-
.ConfigureAwait(false);
196-
197-
_record = new AuthenticationRecord(result, _clientId);
198-
199+
AuthenticationResult result;
200+
if (_record != null)
201+
{
202+
var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _allowMultiTenantAuthentication);
203+
try
204+
{
205+
result = await Client.AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, _record, tenantId, async, cancellationToken)
206+
.ConfigureAwait(false);
207+
return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn));
208+
}
209+
catch (MsalUiRequiredException msalEx)
210+
{
211+
AzureIdentityEventSource.Singleton.UsernamePasswordCredentialAcquireTokenSilentFailed(msalEx);
212+
// fall through so that AuthenticateImplAsync is called.
213+
}
214+
}
215+
result = await AuthenticateImplAsync(async, requestContext, cancellationToken).ConfigureAwait(false);
199216
return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn));
200217
}
201218
catch (Exception e)

sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal class MockMsalPublicClient : MsalPublicClient
2525

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

28-
public Func<string[], IAccount, string, bool, CancellationToken, AuthenticationResult> ExtendedSilentAuthFactory { get; set; }
28+
public Func<string[], string, IAccount, string, bool, CancellationToken, AuthenticationResult> ExtendedSilentAuthFactory { get; set; }
2929

3030
public Func<DeviceCodeInfo, CancellationToken, AuthenticationResult> DeviceCodeAuthFactory { get; set; }
3131

@@ -96,7 +96,7 @@ protected override ValueTask<AuthenticationResult> AcquireTokenSilentCoreAsync(
9696
{
9797
if (ExtendedSilentAuthFactory != null)
9898
{
99-
return new ValueTask<AuthenticationResult>(ExtendedSilentAuthFactory(scopes, account, tenantId, async, cancellationToken));
99+
return new ValueTask<AuthenticationResult>(ExtendedSilentAuthFactory(scopes, claims, account, tenantId, async, cancellationToken));
100100
}
101101

102102
Func<string[], string, AuthenticationResult> factory = SilentAuthFactory ?? AuthFactory;

sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task VerifyAuthenticationRecordOption()
5151
var mockMsalClient = new MockMsalPublicClient
5252
{
5353
Accounts = new List<IAccount> { new MockAccount("[email protected]") },
54-
ExtendedSilentAuthFactory = (_, account, _, _, _) =>
54+
ExtendedSilentAuthFactory = (_, _, account, _, _, _) =>
5555
{
5656
Assert.AreEqual(expectedUsername, account.Username);
5757

@@ -625,7 +625,7 @@ public void TestSetup()
625625
Guid.NewGuid(),
626626
null,
627627
"Bearer");
628-
mockMsal.ExtendedSilentAuthFactory = (_, _, tenant, _, _) =>
628+
mockMsal.ExtendedSilentAuthFactory = (_, _, _, tenant, _, _) =>
629629
{
630630
Assert.AreEqual(expectedTenantId, tenant, "TenantId passed to msal should match");
631631
return result;

sdk/identity/Azure.Identity/tests/UsernamePasswordCredentialTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class UsernamePasswordCredentialTests : ClientTestBase
2323
private MockMsalPublicClient mockMsal;
2424
private DeviceCodeResult deviceCodeResult;
2525
private string expectedTenantId;
26+
private bool interactiveCalled;
27+
private bool silentCalled;
2628

2729
public UsernamePasswordCredentialTests(bool isAsync) : base(isAsync)
2830
{ }
@@ -60,7 +62,14 @@ public void RespectsIsPIILoggingEnabled([Values(true, false)] bool isLoggingPIIE
6062
var clientId = Guid.NewGuid().ToString();
6163
var tenantId = Guid.NewGuid().ToString();
6264

63-
var credential = new UsernamePasswordCredential(username, password, clientId, tenantId, new TokenCredentialOptions{ IsLoggingPIIEnabled = isLoggingPIIEnabled}, default, null);
65+
var credential = new UsernamePasswordCredential(
66+
username,
67+
password,
68+
clientId,
69+
tenantId,
70+
new TokenCredentialOptions { IsLoggingPIIEnabled = isLoggingPIIEnabled },
71+
default,
72+
null);
6473

6574
Assert.NotNull(credential.Client);
6675
Assert.AreEqual(isLoggingPIIEnabled, credential.Client.LogPII);
@@ -82,8 +91,37 @@ public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId,
8291
Assert.AreEqual(expiresOn, token.ExpiresOn);
8392
}
8493

94+
[Test]
95+
public async Task CallsGetAzquireTokenSilentAfterFirstTokenAcquired(
96+
[Values(null, TenantIdHint)] string tenantId,
97+
[Values(true)] bool allowMultiTenantAuthentication)
98+
{
99+
TestSetup();
100+
var options = new UsernamePasswordCredentialOptions { AllowMultiTenantAuthentication = allowMultiTenantAuthentication };
101+
var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId);
102+
expectedTenantId = TenantIdResolver.Resolve(TenantId, context, options.AllowMultiTenantAuthentication);
103+
104+
var credential = InstrumentClient(new UsernamePasswordCredential("user", "password", TenantId, ClientId, options, null, mockMsal));
105+
106+
AccessToken token = await credential.GetTokenAsync(context);
107+
108+
Assert.AreEqual(expectedToken, token.Token);
109+
Assert.AreEqual(expiresOn, token.ExpiresOn);
110+
Assert.True(interactiveCalled);
111+
Assert.False(silentCalled);
112+
113+
// Second call should acquireSilent
114+
token = await credential.GetTokenAsync(context);
115+
116+
Assert.AreEqual(expectedToken, token.Token);
117+
Assert.AreEqual(expiresOn, token.ExpiresOn);
118+
Assert.True(silentCalled);
119+
}
120+
85121
public void TestSetup()
86122
{
123+
interactiveCalled = false;
124+
silentCalled = false;
87125
expectedTenantId = null;
88126
expectedCode = Guid.NewGuid().ToString();
89127
expectedToken = Guid.NewGuid().ToString();
@@ -106,6 +144,14 @@ public void TestSetup()
106144
"Bearer");
107145
mockMsal.UserPassAuthFactory = (_, tenant) =>
108146
{
147+
interactiveCalled = true;
148+
Assert.AreEqual(expectedTenantId, tenant, "TenantId passed to msal should match");
149+
return result;
150+
};
151+
152+
mockMsal.SilentAuthFactory = (_, tenant) =>
153+
{
154+
silentCalled = true;
109155
Assert.AreEqual(expectedTenantId, tenant, "TenantId passed to msal should match");
110156
return result;
111157
};

0 commit comments

Comments
 (0)