diff --git a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs index 807dbc499..8479096f6 100644 --- a/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs +++ b/src/Microsoft.Identity.Web/AppServicesAuth/AppServicesAuthenticationTokenAcquisition.cs @@ -18,7 +18,12 @@ namespace Microsoft.Identity.Web /// public class AppServicesAuthenticationTokenAcquisition : ITokenAcquisition { - private IConfidentialClientApplication? _confidentialClientApplication; + private readonly object _applicationSyncObj = new object(); + + /// + /// Please call GetOrCreateApplication instead of accessing this field directly. + /// + private IConfidentialClientApplication? _application; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IMsalHttpClientFactory _httpClientFactory; private readonly IMsalTokenCacheProvider _tokenCacheProvider; @@ -70,22 +75,28 @@ public AppServicesAuthenticationTokenAcquisition( private IConfidentialClientApplication GetOrCreateApplication() { - if (_confidentialClientApplication == null) + if (_application == null) { - ConfidentialClientApplicationOptions options = new ConfidentialClientApplicationOptions() + lock (_applicationSyncObj) { - ClientId = AppServicesAuthenticationInformation.ClientId, - ClientSecret = AppServicesAuthenticationInformation.ClientSecret, - Instance = AppServicesAuthenticationInformation.Issuer, - }; - _confidentialClientApplication = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(options) - .WithHttpClientFactory(_httpClientFactory) - .Build(); - _tokenCacheProvider.Initialize(_confidentialClientApplication.AppTokenCache); - _tokenCacheProvider.Initialize(_confidentialClientApplication.UserTokenCache); + if (_application == null) + { + var options = new ConfidentialClientApplicationOptions() + { + ClientId = AppServicesAuthenticationInformation.ClientId, + ClientSecret = AppServicesAuthenticationInformation.ClientSecret, + Instance = AppServicesAuthenticationInformation.Issuer, + }; + _application = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(options) + .WithHttpClientFactory(_httpClientFactory) + .Build(); + _tokenCacheProvider.Initialize(_application.AppTokenCache); + _tokenCacheProvider.Initialize(_application.UserTokenCache); + } + } } - return _confidentialClientApplication; + return _application; } /// @@ -109,16 +120,29 @@ public async Task GetAccessTokenForAppAsync( } /// - public async Task GetAccessTokenForUserAsync( + public Task GetAccessTokenForUserAsync( IEnumerable scopes, string? tenantId = null, string? userFlow = null, ClaimsPrincipal? user = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null) { - string accessToken = GetAccessToken(CurrentHttpContext?.Request.Headers); + var httpContext = CurrentHttpContext; + string accessToken; + if (httpContext != null) + { + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + accessToken = GetAccessToken(httpContext.Request.Headers); + } + } + else + { + accessToken = string.Empty; + } - return await Task.FromResult(accessToken).ConfigureAwait(false); + return Task.FromResult(accessToken); } private string GetAccessToken(IHeaderDictionary? headers) @@ -145,7 +169,6 @@ private string GetAccessToken(IHeaderDictionary? headers) } /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task GetAuthenticationResultForUserAsync( IEnumerable scopes, string? tenantId = null, @@ -209,6 +232,5 @@ public Task GetAuthenticationResultForAppAsync(string scop { throw new NotImplementedException(); } -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } } diff --git a/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs b/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs index eaf5234ca..78f96abc6 100644 --- a/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs +++ b/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -101,17 +102,27 @@ public override void OnException(ExceptionContext context) incrementalConsentScopes = Scopes; } + HttpRequest httpRequest; + ClaimsPrincipal user; + HttpContext httpContext = context.HttpContext; + + lock (httpContext) + { + httpRequest = httpContext.Request; + user = httpContext.User; + } + AuthenticationProperties properties = IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties( incrementalConsentScopes, msalUiRequiredException, - context.HttpContext.User, + user, UserFlow); - if (IsAjaxRequest(context.HttpContext.Request) && (!string.IsNullOrEmpty(context.HttpContext.Request.Headers[Constants.XReturnUrl]) - || !string.IsNullOrEmpty(context.HttpContext.Request.Query[Constants.XReturnUrl]))) + if (IsAjaxRequest(httpRequest) && (!string.IsNullOrEmpty(httpRequest.Headers[Constants.XReturnUrl]) + || !string.IsNullOrEmpty(httpRequest.Query[Constants.XReturnUrl]))) { - string redirectUri = !string.IsNullOrEmpty(context.HttpContext.Request.Headers[Constants.XReturnUrl]) ? context.HttpContext.Request.Headers[Constants.XReturnUrl] - : context.HttpContext.Request.Query[Constants.XReturnUrl]; + string redirectUri = !string.IsNullOrEmpty(httpRequest.Headers[Constants.XReturnUrl]) ? httpRequest.Headers[Constants.XReturnUrl] + : httpRequest.Query[Constants.XReturnUrl]; UrlHelper urlHelper = new UrlHelper(context); if (urlHelper.IsLocalUrl(redirectUri)) diff --git a/src/Microsoft.Identity.Web/HttpContextExtensions.cs b/src/Microsoft.Identity.Web/HttpContextExtensions.cs index cc7a5c220..fb23b3baa 100644 --- a/src/Microsoft.Identity.Web/HttpContextExtensions.cs +++ b/src/Microsoft.Identity.Web/HttpContextExtensions.cs @@ -16,7 +16,11 @@ internal static class HttpContextExtensions /// it can be used in the actions. internal static void StoreTokenUsedToCallWebAPI(this HttpContext httpContext, JwtSecurityToken? token) { - httpContext.Items.Add(Constants.JwtSecurityTokenUsedToCallWebApi, token); + // lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + httpContext.Items.Add(Constants.JwtSecurityTokenUsedToCallWebApi, token); + } } /// @@ -26,7 +30,11 @@ internal static void StoreTokenUsedToCallWebAPI(this HttpContext httpContext, Jw /// used to call the web API. internal static JwtSecurityToken? GetTokenUsedToCallWebAPI(this HttpContext httpContext) { - return httpContext.Items[Constants.JwtSecurityTokenUsedToCallWebApi] as JwtSecurityToken; + // lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + return httpContext.Items[Constants.JwtSecurityTokenUsedToCallWebApi] as JwtSecurityToken; + } } } } diff --git a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml index c5d466661..58d0bcbe8 100644 --- a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml +++ b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml @@ -123,6 +123,11 @@ Implementation of ITokenAcquisition for App Services authentication (EasyAuth). + + + Please call GetOrCreateApplication instead of accessing this field directly. + + Constructor of the AppServicesAuthenticationTokenAcquisition. @@ -1989,6 +1994,11 @@ Token acquisition service. + + + Please call GetOrBuildConfidentialClientApplication instead of accessing this field directly. + + Constructor of the TokenAcquisition service. This requires the Azure AD Options to @@ -2151,11 +2161,6 @@ OpenID Connect event. A that represents a completed account removal operation. - - - Creates an MSAL confidential client application, if needed. - - Creates an MSAL confidential client application. diff --git a/src/Microsoft.Identity.Web/MicrosoftIdentityConsentAndConditionalAccessHandler.cs b/src/Microsoft.Identity.Web/MicrosoftIdentityConsentAndConditionalAccessHandler.cs index c6dee66a4..7623d7052 100644 --- a/src/Microsoft.Identity.Web/MicrosoftIdentityConsentAndConditionalAccessHandler.cs +++ b/src/Microsoft.Identity.Web/MicrosoftIdentityConsentAndConditionalAccessHandler.cs @@ -43,11 +43,21 @@ public ClaimsPrincipal User { get { - return _user ?? -#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. - (!IsBlazorServer ? _httpContextAccessor.HttpContext.User : -#pragma warning restore CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. - throw new InvalidOperationException(IDWebErrorMessage.BlazorServerUserNotSet)); + if (_user != null) + { + return _user; + } + + HttpContext httpContext = _httpContextAccessor!.HttpContext!; + ClaimsPrincipal user; + + lock (httpContext) + { + user = httpContext.User; + } + + return !IsBlazorServer ? user : + throw new InvalidOperationException(IDWebErrorMessage.BlazorServerUserNotSet); } set { @@ -62,11 +72,21 @@ public string? BaseUri { get { - return _baseUri ?? -#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case - (!IsBlazorServer ? CreateBaseUri(_httpContextAccessor.HttpContext.Request) : -#pragma warning restore CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case - throw new InvalidOperationException(IDWebErrorMessage.BlazorServerBaseUriNotSet)); + if (_baseUri != null) + { + return _baseUri; + } + + HttpRequest httpRequest; + HttpContext httpContext = _httpContextAccessor!.HttpContext!; + + lock (httpContext) + { + httpRequest = httpContext.Request; + } + + return !IsBlazorServer ? CreateBaseUri(httpRequest) : + throw new InvalidOperationException(IDWebErrorMessage.BlazorServerBaseUriNotSet); } set { @@ -160,14 +180,19 @@ public void ChallengeUser( } else { -#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. - var request = _httpContextAccessor.HttpContext.Request; -#pragma warning restore CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. + HttpRequest httpRequest; + HttpContext httpContext = _httpContextAccessor!.HttpContext!; + + lock (httpContext) + { + httpRequest = httpContext.Request; + } + redirectUri = string.Format( CultureInfo.InvariantCulture, "{0}/{1}", - CreateBaseUri(request), - request.Path.ToString().TrimStart('/')); + CreateBaseUri(httpRequest), + httpRequest.Path.ToString().TrimStart('/')); } string url = $"{BaseUri}/{Constants.BlazorChallengeUri}{redirectUri}" @@ -181,9 +206,12 @@ public void ChallengeUser( } else { -#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. - _httpContextAccessor.HttpContext.Response.Redirect(url); -#pragma warning restore CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case. + HttpContext httpContext = _httpContextAccessor!.HttpContext!; + + lock (httpContext) + { + httpContext.Response.Redirect(url); + } } } diff --git a/src/Microsoft.Identity.Web/Resource/RequiredScopeFilter.cs b/src/Microsoft.Identity.Web/Resource/RequiredScopeFilter.cs index 61af440fe..5a2a4e470 100644 --- a/src/Microsoft.Identity.Web/Resource/RequiredScopeFilter.cs +++ b/src/Microsoft.Identity.Web/Resource/RequiredScopeFilter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -55,31 +56,51 @@ private void ValidateEffectiveScopes(AuthorizationFilterContext context) throw new InvalidOperationException(IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter); } - if (context.HttpContext.User == null || context.HttpContext.User.Claims == null || !context.HttpContext.User.Claims.Any()) + IEnumerable userClaims; + ClaimsPrincipal user; + HttpContext httpContext = context.HttpContext; + + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + user = httpContext.User; + userClaims = user.Claims; + } + + if (user == null || userClaims == null || !userClaims.Any()) { - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + lock (httpContext) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } + throw new UnauthorizedAccessException(IDWebErrorMessage.UnauthenticatedUser); } else { // Attempt with Scp claim - Claim? scopeClaim = context.HttpContext.User.FindFirst(ClaimConstants.Scp); + Claim? scopeClaim = user.FindFirst(ClaimConstants.Scp); // Fallback to Scope claim name if (scopeClaim == null) { - scopeClaim = context.HttpContext.User.FindFirst(ClaimConstants.Scope); + scopeClaim = user.FindFirst(ClaimConstants.Scope); } if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(_effectiveAcceptedScopes).Any()) { - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; string message = string.Format( CultureInfo.InvariantCulture, IDWebErrorMessage.MissingScopes, string.Join(",", _effectiveAcceptedScopes)); - context.HttpContext.Response.WriteAsync(message); - context.HttpContext.Response.CompleteAsync(); + + lock (httpContext) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + httpContext.Response.WriteAsync(message); + httpContext.Response.CompleteAsync(); + } + throw new UnauthorizedAccessException(message); } } diff --git a/src/Microsoft.Identity.Web/Resource/RolesRequiredHttpContextExtensions.cs b/src/Microsoft.Identity.Web/Resource/RolesRequiredHttpContextExtensions.cs index c1ab2bb0c..6aaec4c59 100644 --- a/src/Microsoft.Identity.Web/Resource/RolesRequiredHttpContextExtensions.cs +++ b/src/Microsoft.Identity.Web/Resource/RolesRequiredHttpContextExtensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Claims; using Microsoft.AspNetCore.Http; namespace Microsoft.Identity.Web.Resource @@ -36,24 +37,44 @@ public static void ValidateAppRole(this HttpContext context, params string[] acc { throw new ArgumentNullException(nameof(context)); } - else if (context.User == null || context.User.Claims == null || !context.User.Claims.Any()) + + IEnumerable userClaims; + ClaimsPrincipal user; + + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (context) + { + user = context.User; + userClaims = user.Claims; + } + + if (user == null || userClaims == null || !userClaims.Any()) { - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } + throw new UnauthorizedAccessException(IDWebErrorMessage.UnauthenticatedUser); } else { // Attempt with Roles claim - IEnumerable rolesClaim = context.User.Claims.Where( + IEnumerable rolesClaim = userClaims.Where( c => c.Type == ClaimConstants.Roles || c.Type == ClaimConstants.Role) .SelectMany(c => c.Value.Split(' ')); if (!rolesClaim.Intersect(acceptedRoles).Any()) { - context.Response.StatusCode = (int)HttpStatusCode.Forbidden; string message = string.Format(CultureInfo.InvariantCulture, IDWebErrorMessage.MissingRoles, string.Join(", ", acceptedRoles)); - context.Response.WriteAsync(message); - context.Response.CompleteAsync(); + + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + context.Response.WriteAsync(message); + context.Response.CompleteAsync(); + } + throw new UnauthorizedAccessException(message); } } diff --git a/src/Microsoft.Identity.Web/Resource/ScopesRequiredHttpContextExtensions.cs b/src/Microsoft.Identity.Web/Resource/ScopesRequiredHttpContextExtensions.cs index 9045b31b2..5d973590b 100644 --- a/src/Microsoft.Identity.Web/Resource/ScopesRequiredHttpContextExtensions.cs +++ b/src/Microsoft.Identity.Web/Resource/ScopesRequiredHttpContextExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -43,28 +44,48 @@ public static void VerifyUserHasAnyAcceptedScope(this HttpContext context, param { throw new ArgumentNullException(nameof(context)); } - else if (context.User == null || context.User.Claims == null || !context.User.Claims.Any()) + + IEnumerable userClaims; + ClaimsPrincipal user; + + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (context) + { + user = context.User; + userClaims = user.Claims; + } + + if (user == null || userClaims == null || !userClaims.Any()) { - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } + throw new UnauthorizedAccessException(IDWebErrorMessage.UnauthenticatedUser); } else { // Attempt with Scp claim - Claim? scopeClaim = context.User.FindFirst(ClaimConstants.Scp); + Claim? scopeClaim = user.FindFirst(ClaimConstants.Scp); // Fallback to Scope claim name if (scopeClaim == null) { - scopeClaim = context.User.FindFirst(ClaimConstants.Scope); + scopeClaim = user.FindFirst(ClaimConstants.Scope); } if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(acceptedScopes).Any()) { - context.Response.StatusCode = (int)HttpStatusCode.Forbidden; string message = string.Format(CultureInfo.InvariantCulture, IDWebErrorMessage.MissingScopes, string.Join(",", acceptedScopes)); - context.Response.WriteAsync(message); - context.Response.CompleteAsync(); + + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + context.Response.WriteAsync(message); + context.Response.CompleteAsync(); + } + throw new UnauthorizedAccessException(message); } } diff --git a/src/Microsoft.Identity.Web/TokenAcquisition.cs b/src/Microsoft.Identity.Web/TokenAcquisition.cs index 5cd0f5b00..0920ada52 100644 --- a/src/Microsoft.Identity.Web/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web/TokenAcquisition.cs @@ -33,6 +33,11 @@ internal class TokenAcquisition : ITokenAcquisitionInternal private readonly ConfidentialClientApplicationOptions _applicationOptions; private readonly IMsalTokenCacheProvider _tokenCacheProvider; + private readonly object _applicationSyncObj = new object(); + + /// + /// Please call GetOrBuildConfidentialClientApplication instead of accessing this field directly. + /// private IConfidentialClientApplication? _application; private readonly IHttpContextAccessor _httpContextAccessor; private HttpContext? CurrentHttpContext => _httpContextAccessor.HttpContext; @@ -144,12 +149,12 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsync( try { - _application = GetOrBuildConfidentialClientApplication(); + var application = GetOrBuildConfidentialClientApplication(); // Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it and will not send the OAuth 2.0 request in // case a further call to AcquireTokenByAuthorizationCodeAsync in the future is required for incremental consent (getting a code requesting more scopes) // Share the ID token though - var builder = _application + var builder = application .AcquireTokenByAuthorizationCode(scopes.Except(_scopesRequestedByMsal), context.ProtocolMessage.Code) .WithSendX5C(_microsoftIdentityOptions.SendX5C); @@ -210,17 +215,15 @@ public async Task GetAuthenticationResultForUserAsync( user = await GetAuthenticatedUserAsync(user).ConfigureAwait(false); - _application = GetOrBuildConfidentialClientApplication(); + var application = GetOrBuildConfidentialClientApplication(); - string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenantId); - - AuthenticationResult? authenticationResult; + string authority = CreateAuthorityBasedOnTenantIfProvided(application, tenantId); try { // Access token will return if call is from a web API - authenticationResult = await GetAuthenticationResultForWebApiToCallDownstreamApiAsync( - _application, + var authenticationResult = await GetAuthenticationResultForWebApiToCallDownstreamApiAsync( + application, authority, scopes, tokenAcquisitionOptions).ConfigureAwait(false); @@ -232,7 +235,7 @@ public async Task GetAuthenticationResultForUserAsync( // If access token is null, this is a web app return await GetAuthenticationResultForWebAppWithAccountFromCacheAsync( - _application, + application, user, scopes, authority, @@ -289,10 +292,10 @@ public Task GetAuthenticationResultForAppAsync( } // Use MSAL to get the right token to call the API - _application = GetOrBuildConfidentialClientApplication(); - string authority = CreateAuthorityBasedOnTenantIfProvided(_application, tenant); + var application = GetOrBuildConfidentialClientApplication(); + string authority = CreateAuthorityBasedOnTenantIfProvided(application, tenant); - var builder = _application + var builder = application .AcquireTokenForClient(new string[] { scope }.Except(_scopesRequestedByMsal)) .WithSendX5C(_microsoftIdentityOptions.SendX5C) .WithAuthority(authority); @@ -381,14 +384,13 @@ await GetAuthenticationResultForUserAsync( /// Scopes to consent to. /// The that triggered the challenge. /// The to update. -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync( + public Task ReplyForbiddenWithWwwAuthenticateHeaderAsync( IEnumerable scopes, MsalUiRequiredException msalServiceException, HttpResponse? httpResponse = null) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { ReplyForbiddenWithWwwAuthenticateHeader(scopes, msalServiceException, httpResponse); + return Task.CompletedTask; } /// @@ -411,10 +413,10 @@ public void ReplyForbiddenWithWwwAuthenticateHeader( throw msalServiceException; } - _application = GetOrBuildConfidentialClientApplication(); + var application = GetOrBuildConfidentialClientApplication(); - string consentUrl = $"{_application.Authority}/oauth2/v2.0/authorize?client_id={_applicationOptions.ClientId}" - + $"&response_type=code&redirect_uri={_application.AppConfig.RedirectUri}" + string consentUrl = $"{application.Authority}/oauth2/v2.0/authorize?client_id={_applicationOptions.ClientId}" + + $"&response_type=code&redirect_uri={application.AppConfig.RedirectUri}" + $"&response_mode=query&scope=offline_access%20{string.Join("%20", scopes)}"; IDictionary parameters = new Dictionary() @@ -472,14 +474,31 @@ public async Task RemoveAccountAsync(RedirectContext context) } } - /// - /// Creates an MSAL confidential client application, if needed. - /// + private string BuildCurrentUriFromRequest(HttpContext httpContext, HttpRequest request) + { + // need to lock to avoid threading issues with code outside of this library + // https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + return UriHelper.BuildAbsolute( + request.Scheme, + request.Host, + request.PathBase, + _microsoftIdentityOptions.CallbackPath.Value ?? string.Empty); + } + } + internal /* for testing */ IConfidentialClientApplication GetOrBuildConfidentialClientApplication() { if (_application == null) { - return BuildConfidentialClientApplication(); + lock (_applicationSyncObj) + { + if (_application == null) + { + _application = BuildConfidentialClientApplication(); + } + } } return _application; @@ -490,7 +509,8 @@ public async Task RemoveAccountAsync(RedirectContext context) /// private IConfidentialClientApplication BuildConfidentialClientApplication() { - var request = CurrentHttpContext?.Request; + var httpContext = CurrentHttpContext; + var request = httpContext?.Request; string? currentUri = null; if (!string.IsNullOrEmpty(_applicationOptions.RedirectUri)) @@ -500,11 +520,7 @@ private IConfidentialClientApplication BuildConfidentialClientApplication() if (request != null && string.IsNullOrEmpty(currentUri)) { - currentUri = UriHelper.BuildAbsolute( - request.Scheme, - request.Host, - request.PathBase, - _microsoftIdentityOptions.CallbackPath.Value ?? string.Empty); + currentUri = BuildCurrentUriFromRequest(httpContext!, request); } PrepareAuthorityInstanceForMsal(); @@ -595,11 +611,11 @@ private void PrepareAuthorityInstanceForMsal() string tokenUsedToCallTheWebApi = validatedToken.InnerToken == null ? validatedToken.RawData : validatedToken.InnerToken.RawData; var builder = application - .AcquireTokenOnBehalfOf( - scopes.Except(_scopesRequestedByMsal), - new UserAssertion(tokenUsedToCallTheWebApi)) - .WithSendX5C(_microsoftIdentityOptions.SendX5C) - .WithAuthority(authority); + .AcquireTokenOnBehalfOf( + scopes.Except(_scopesRequestedByMsal), + new UserAssertion(tokenUsedToCallTheWebApi)) + .WithSendX5C(_microsoftIdentityOptions.SendX5C) + .WithAuthority(authority); if (tokenAcquisitionOptions != null) { @@ -686,7 +702,7 @@ private async Task GetAuthenticationResultForWebAppWithAcc /// on behalf of the user. /// Azure AD B2C user flow. /// Options passed-in to create the token acquisition object which calls into MSAL .NET. - private async Task GetAuthenticationResultForWebAppWithAccountFromCacheAsync( + private Task GetAuthenticationResultForWebAppWithAccountFromCacheAsync( IConfidentialClientApplication application, IAccount? account, IEnumerable scopes, @@ -730,8 +746,7 @@ private async Task GetAuthenticationResultForWebAppWithAcc builder.WithAuthority(authority); } - return await builder.ExecuteAsync() - .ConfigureAwait(false); + return builder.ExecuteAsync(); } private static bool AcceptedTokenVersionMismatch(MsalUiRequiredException msalServiceException) @@ -745,11 +760,26 @@ private static bool AcceptedTokenVersionMismatch(MsalUiRequiredException msalSer StringComparison.InvariantCulture); } + private ClaimsPrincipal? GetUserFromHttpContext() + { + var httpContext = CurrentHttpContext; + if (httpContext != null) + { + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (httpContext) + { + return httpContext.User; + } + } + + return null; + } + private async Task GetAuthenticatedUserAsync(ClaimsPrincipal? user) { - if (user == null && _httpContextAccessor.HttpContext?.User != null) + if (user == null) { - user = _httpContextAccessor.HttpContext.User; + user = GetUserFromHttpContext(); } if (user == null)