Skip to content

Commit e6d762a

Browse files
committed
Fix for #5375 - Add a FormatResultAsync to IAuthenticationOperation
1 parent 39c0eb0 commit e6d762a

27 files changed

+241
-73
lines changed

src/client/Microsoft.Identity.Client/AuthScheme/Bearer/BearerAuthenticationOperation.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Generic;
5+
using System.Threading;
6+
using System.Threading.Tasks;
57
using Microsoft.Identity.Client.Cache.Items;
68
using Microsoft.Identity.Client.Internal;
79
using Microsoft.Identity.Client.Utils;
810

911
namespace Microsoft.Identity.Client.AuthScheme.Bearer
1012
{
11-
internal class BearerAuthenticationOperation : IAuthenticationOperation
13+
internal class BearerAuthenticationOperation : IAuthenticationOperation2
1214
{
1315
internal const string BearerTokenType = "bearer";
1416

@@ -25,6 +27,12 @@ public void FormatResult(AuthenticationResult authenticationResult)
2527
// no-op
2628
}
2729

30+
public Task FormatResultAsync(AuthenticationResult authenticationResult, CancellationToken cancellationToken = default)
31+
{
32+
// no-op, return completed task
33+
return Task.CompletedTask;
34+
}
35+
2836
public IReadOnlyDictionary<string, string> GetTokenRequestParams()
2937
{
3038
// ESTS issues Bearer tokens by default, no need for any extra params

src/client/Microsoft.Identity.Client/AuthScheme/IAuthenticationOperation.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Generic;
5+
using System.Threading.Tasks;
56
using Microsoft.Identity.Client.Cache.Items;
67

78
namespace Microsoft.Identity.Client.AuthScheme
@@ -54,7 +55,7 @@ public interface IAuthenticationOperation
5455
/// Creates the access token that goes into an Authorization HTTP header.
5556
/// </summary>
5657
void FormatResult(AuthenticationResult authenticationResult);
57-
58+
5859
/// <summary>
5960
/// Expected to match the token_type parameter returned by ESTS. Used to disambiguate
6061
/// between ATs of different types (e.g. Bearer and PoP) when loading from cache etc.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Identity.Client.AuthScheme
8+
{
9+
/// <summary>
10+
/// This is an extensibility API and should only be used by SDKs.
11+
/// Enhanced version of IAuthenticationOperation that supports asynchronous token formatting.
12+
/// Used to modify the experience depending on the type of token asked with async capabilities.
13+
/// </summary>
14+
public interface IAuthenticationOperation2 : IAuthenticationOperation
15+
{
16+
/// <summary>
17+
/// Will be invoked instead of IAuthenticationOperation.FormatResult
18+
/// </summary>
19+
Task FormatResultAsync(AuthenticationResult authenticationResult, CancellationToken cancellationToken = default);
20+
}
21+
}

src/client/Microsoft.Identity.Client/AuthScheme/PoP/PopAuthenticationOperation.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using System.Globalization;
77
using System.Security.Cryptography;
88
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
911
using Microsoft.Identity.Client.AppConfig;
1012
using Microsoft.Identity.Client.Cache.Items;
1113
using Microsoft.Identity.Client.Internal;
@@ -21,7 +23,7 @@
2123

2224
namespace Microsoft.Identity.Client.AuthScheme.PoP
2325
{
24-
internal class PopAuthenticationOperation : IAuthenticationOperation
26+
internal class PopAuthenticationOperation : IAuthenticationOperation2
2527
{
2628
private readonly PoPAuthenticationConfiguration _popAuthenticationConfiguration;
2729
private readonly IPoPCryptoProvider _popCryptoProvider;
@@ -86,6 +88,14 @@ public void FormatResult(AuthenticationResult authenticationResult)
8688
authenticationResult.AccessToken = popToken;
8789
}
8890

91+
public Task FormatResultAsync(AuthenticationResult authenticationResult, CancellationToken cancellationToken = default)
92+
{
93+
// For now, PoP token creation is synchronous, so we wrap the sync method
94+
// Future enhancement could make crypto operations truly async
95+
FormatResult(authenticationResult);
96+
return Task.CompletedTask;
97+
}
98+
8999
private JObject CreateBody(string accessToken)
90100
{
91101
var publicKeyJwk = JToken.Parse(_popCryptoProvider.CannonicalPublicKeyJwk);

src/client/Microsoft.Identity.Client/AuthenticationResult.cs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.ComponentModel;
77
using System.Globalization;
88
using System.Security.Claims;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.Identity.Client.AuthScheme;
1112
using Microsoft.Identity.Client.Cache;
@@ -21,7 +22,7 @@ namespace Microsoft.Identity.Client
2122
/// </summary>
2223
public partial class AuthenticationResult
2324
{
24-
private readonly IAuthenticationOperation _authenticationScheme;
25+
private IAuthenticationOperation _authenticationScheme;
2526

2627
/// <summary>
2728
/// Constructor meant to help application developers test their apps. Allows mocking of authentication flows.
@@ -126,19 +127,76 @@ public AuthenticationResult(
126127

127128
}
128129

129-
internal AuthenticationResult(
130+
/// <summary>
131+
/// This method must be used by the product code to create an <see cref="AuthenticationResult"/> instance.
132+
/// It calls IAuthenticationOperation.FormatResult or FormatResultAsync on the authentication scheme
133+
/// </summary>
134+
internal static async Task<AuthenticationResult> CreateAsync(
130135
MsalAccessTokenCacheItem msalAccessTokenCacheItem,
131136
MsalIdTokenCacheItem msalIdTokenCacheItem,
132137
IAuthenticationOperation authenticationScheme,
133-
Guid correlationID,
138+
Guid correlationId,
134139
TokenSource tokenSource,
135140
ApiEvent apiEvent,
136141
Account account,
137142
string spaAuthCode,
138-
IReadOnlyDictionary<string, string> additionalResponseParameters)
143+
IReadOnlyDictionary<string, string> additionalResponseParameters,
144+
CancellationToken cancellationToken = default)
139145
{
140-
_authenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));
146+
if (authenticationScheme == null)
147+
{
148+
throw new ArgumentNullException(nameof(authenticationScheme));
149+
}
141150

151+
// Create the AuthenticationResult without calling FormatResult in constructor
152+
var result = new AuthenticationResult(
153+
msalAccessTokenCacheItem,
154+
msalIdTokenCacheItem,
155+
correlationId,
156+
tokenSource,
157+
apiEvent,
158+
account,
159+
spaAuthCode,
160+
additionalResponseParameters,
161+
authenticationScheme);
162+
163+
// Apply token formatting (async if supported, sync otherwise)
164+
var measuredResultDuration = await StopwatchService.MeasureCodeBlockAsync(async () =>
165+
{
166+
if (authenticationScheme is IAuthenticationOperation2 asyncAuthScheme)
167+
{
168+
await asyncAuthScheme.FormatResultAsync(result, cancellationToken).ConfigureAwait(false);
169+
}
170+
else
171+
{
172+
authenticationScheme.FormatResult(result);
173+
}
174+
}).ConfigureAwait(false);
175+
176+
// Update telemetry metadata
177+
result.AuthenticationResultMetadata.DurationCreatingExtendedTokenInUs = measuredResultDuration.Microseconds;
178+
result.AuthenticationResultMetadata.TelemetryTokenType = authenticationScheme.TelemetryTokenType;
179+
180+
return result;
181+
}
182+
183+
//Default constructor for testing
184+
internal AuthenticationResult() { }
185+
186+
/// <summary>
187+
/// This is to help CreateAsync
188+
/// </summary>
189+
private AuthenticationResult(
190+
MsalAccessTokenCacheItem msalAccessTokenCacheItem,
191+
MsalIdTokenCacheItem msalIdTokenCacheItem,
192+
Guid correlationID,
193+
TokenSource tokenSource,
194+
ApiEvent apiEvent,
195+
Account account,
196+
string spaAuthCode,
197+
IReadOnlyDictionary<string, string> additionalResponseParameters,
198+
IAuthenticationOperation authenticationScheme)
199+
{
142200
string homeAccountId =
143201
msalAccessTokenCacheItem?.HomeAccountId ??
144202
msalIdTokenCacheItem?.HomeAccountId;
@@ -163,7 +221,7 @@ internal AuthenticationResult(
163221
TenantId = msalIdTokenCacheItem?.IdToken?.TenantId;
164222
IdToken = msalIdTokenCacheItem?.Secret;
165223
SpaAuthCode = spaAuthCode;
166-
224+
_authenticationScheme = authenticationScheme;
167225
CorrelationId = correlationID;
168226
ApiEvent = apiEvent;
169227
AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource);
@@ -190,19 +248,10 @@ internal AuthenticationResult(
190248
AccessToken = msalAccessTokenCacheItem.Secret;
191249
}
192250

193-
var measuredResultDuration = StopwatchService.MeasureCodeBlock(() =>
194-
{
195-
//Important: only call this at the end
196-
authenticationScheme.FormatResult(this);
197-
});
198-
199-
AuthenticationResultMetadata.DurationCreatingExtendedTokenInUs = measuredResultDuration.Microseconds;
200-
AuthenticationResultMetadata.TelemetryTokenType = authenticationScheme.TelemetryTokenType;
251+
// Note: Token formatting is NOT performed in this constructor.
252+
// The caller (AuthenticationResultFactory) is responsible for calling FormatResult/FormatResultAsync
201253
}
202254

203-
//Default constructor for testing
204-
internal AuthenticationResult() { }
205-
206255
/// <summary>
207256
/// Access Token that can be used as a bearer token to access protected web APIs
208257
/// </summary>

src/client/Microsoft.Identity.Client/Extensibility/MsalAuthenticationExtension.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class MsalAuthenticationExtension
2020
public Func<OnBeforeTokenRequestData, Task> OnBeforeTokenRequestHandler { get; set; }
2121

2222
/// <summary>
23-
/// Enables the developer to provide a custom authentication extension.
23+
/// Important: IAuthenticationOperation2 exists, for asynchronous token formatting.
2424
/// </summary>
2525
public IAuthenticationOperation AuthenticationOperation { get; set; }
2626

src/client/Microsoft.Identity.Client/Internal/Requests/ByRefreshTokenRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
3737
throw new MsalServiceException(msalTokenResponse.Error, msalTokenResponse.ErrorDescription, null);
3838
}
3939

40-
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
40+
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false);
4141
}
4242

4343
private static Dictionary<string, string> GetBodyParameters(string refreshTokenSecret)

src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
7676
// No access token or cached access token needs to be refreshed
7777
if (cachedAccessTokenItem != null)
7878
{
79-
authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
79+
authResult = await CreateAuthenticationResultFromCacheAsync(cachedAccessTokenItem).ConfigureAwait(false);
8080

8181
try
8282
{
@@ -101,7 +101,7 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
101101
}
102102
catch (MsalServiceException e)
103103
{
104-
return await HandleTokenRefreshErrorAsync(e, cachedAccessTokenItem).ConfigureAwait(false);
104+
return await HandleTokenRefreshErrorAsync(e, cachedAccessTokenItem, cancellationToken).ConfigureAwait(false);
105105
}
106106
}
107107
else
@@ -128,7 +128,7 @@ private async Task<AuthenticationResult> GetAccessTokenAsync(
128128
if (ServiceBundle.Config.AppTokenProvider == null)
129129
{
130130
MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(), cancellationToken).ConfigureAwait(false);
131-
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
131+
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false);
132132
}
133133

134134
// Get a token from the app provider delegate
@@ -164,7 +164,7 @@ private async Task<AuthenticationResult> GetAccessTokenAsync(
164164
else
165165
{
166166
logger.Verbose(() => "[ClientCredentialRequest] Checking for a cached access token.");
167-
authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
167+
authResult = await CreateAuthenticationResultFromCacheAsync(cachedAccessTokenItem).ConfigureAwait(false);
168168
}
169169
}
170170

@@ -196,7 +196,7 @@ private async Task<AuthenticationResult> SendTokenRequestToAppTokenProviderAsync
196196
tokenResponse.Scope = appTokenProviderParameters.Scopes.AsSingleString();
197197
tokenResponse.CorrelationId = appTokenProviderParameters.CorrelationId;
198198

199-
AuthenticationResult authResult = await CacheTokenResponseAndCreateAuthenticationResultAsync(tokenResponse)
199+
AuthenticationResult authResult = await CacheTokenResponseAndCreateAuthenticationResultAsync(tokenResponse, cancellationToken)
200200
.ConfigureAwait(false);
201201

202202
return authResult;
@@ -274,9 +274,9 @@ private void MarkAccessTokenAsCacheHit()
274274
/// </summary>
275275
/// <param name="cachedAccessTokenItem"></param>
276276
/// <returns></returns>
277-
private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem)
277+
private Task<AuthenticationResult> CreateAuthenticationResultFromCacheAsync(MsalAccessTokenCacheItem cachedAccessTokenItem)
278278
{
279-
AuthenticationResult authResult = new AuthenticationResult(
279+
return AuthenticationResult.CreateAsync(
280280
cachedAccessTokenItem,
281281
null,
282282
AuthenticationRequestParameters.AuthenticationScheme,
@@ -286,7 +286,6 @@ private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessToken
286286
account: null,
287287
spaAuthCode: null,
288288
additionalResponseParameters: null);
289-
return authResult;
290289
}
291290

292291
/// <summary>

src/client/Microsoft.Identity.Client/Internal/Requests/ConfidentialAuthCodeRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
2828
{
2929
await ResolveAuthorityAsync().ConfigureAwait(false);
3030
var msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(), cancellationToken).ConfigureAwait(false);
31-
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
31+
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false);
3232
}
3333

3434
private Dictionary<string, string> GetBodyParameters()

src/client/Microsoft.Identity.Client/Internal/Requests/DeviceCodeRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
5858
await _deviceCodeParameters.DeviceCodeResultCallback(deviceCodeResult).ConfigureAwait(false);
5959

6060
var msalTokenResponse = await WaitForTokenResponseAsync(deviceCodeResult, cancellationToken).ConfigureAwait(false);
61-
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
61+
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse, cancellationToken).ConfigureAwait(false);
6262
}
6363

6464
private async Task<MsalTokenResponse> WaitForTokenResponseAsync(

0 commit comments

Comments
 (0)