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

[do not merge] feedback PR for perf improvements #1084

Closed
wants to merge 1 commit into from

Conversation

jennyf19
Copy link
Collaborator

No description provided.

idToken = headers[AppServicesAuthIdTokenHeader];
}
string? idToken = null;
if (headers.ContainsKey(AppServicesAuthIdTokenHeader))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryGetValue results in a single dictionary lookup rather than two lookups when using ContainsKey with subsequent access.

Suggested change
if (headers.ContainsKey(AppServicesAuthIdTokenHeader))
if (headers.TryGetValue(AppServicesAuthIdTokenHeader, out var idToken))

@sonarcloud
Copy link

sonarcloud bot commented Mar 24, 2021

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 7057 Code Smells

No Coverage information No Coverage information
0.0% 0.0% Duplication

ClaimsPrincipal? user = null,
TokenAcquisitionOptions? tokenAcquisitionOptions = null)
{
string? idToken = AppServicesAuthenticationInformation.GetIdToken(CurrentHttpContext?.Request?.Headers!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CurrentHttpContext needs to be locked on to avoid threading issues with external code.

string accessToken = await GetAccessTokenForUserAsync(scopes, tenantId, userFlow, user, tokenAcquisitionOptions).ConfigureAwait(false);
string expiration = userClaims.FindFirstValue("exp");
DateTimeOffset dateTimeOffset = (expiration != null)
? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiration, CultureInfo.InvariantCulture))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

long.parse could throw if the claim has a broken expiration string. long.TryParse would be safer.

dateTimeOffset,
dateTimeOffset,
userClaims?.GetTenantId(),
userClaims != null ? new Account(userClaims) : null,
Copy link
Contributor

@DavidParks8 DavidParks8 Mar 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are three null checks on userClaims here.
userClaims?.GetDisplayName()
userClaims?.GetTenantId()
userClaims != null

It would be faster to do the check once and set three variables.

string? displayName;
string? tenantId;
Account? account;
if (userClaims != null)
{
   displayName = userClaims.GetDisplayName();
   tenantId = userClaims.GetTenantId();
   account = new Account(userClaims);
}
else
{
   displayName = null;
   tenantId = null;
   account = null;
}

}

/// <inheritdoc/>
public async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync(IEnumerable<string> scopes, MsalUiRequiredException msalServiceException, HttpResponse? httpResponse = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the async keyword can be removed when all the function does is throw. Doing so would avoid the need to suppress a warning about async functions needing something awaited.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I already did that in this file in my other PR.

var userFlow = options.DefaultUserFlow;
return new Uri(baseUri, new PathString($"{pathBase}/{domain}/{userFlow}/v2.0")).ToString();
}
else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really matter that much, but the else keyword and block nesting isn't needed because the if (options.IsB2C) block had a return statement.

internal static string EnsureAuthorityIsV2(string authority)
{
authority = authority.Trim().TrimEnd('/');
if (!authority.EndsWith("v2.0", StringComparison.InvariantCulture))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For strings that are language and culture agnostic, StringComparison.Ordinal should be used. It is faster and more targeted to the usecase here: https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings

}
internal static string EnsureAuthorityIsV2(string authority)
{
authority = authority.Trim().TrimEnd('/');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String allocations tend to be a death by 1000 cuts issue at scale. Instead of TrimEnd('/'), I suggest adding "v2.0" or "/v2.0" based upon the presence of the trailing slash. Doing so should skip one of the string reallocations.

throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
IDWebErrorMessage.ProvideEitherScopeKeySectionOrScopes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages should really be localized in order to promote the most inclusive api experiences.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once localized, InvariantCulture is probably not the correct culture to use.

if (!string.IsNullOrWhiteSpace(ScopeKeySection))
{
// Load the injected IConfiguration
IConfiguration configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRequiredService will throw if the service isn't registered. I recommend changing it to GetService to avoid an unhelpful error.

}
else
{
if (Options.IsB2C)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if block can be less nested by combining towards "else if" which will slightly improve readability.

// Correlation ID: f99deff4-f43b-43cc-b4e7-36141dbaf0a0
// Timestamp: 2018-03-05 02:49:35Z
// ', error_uri: 'error_uri is null'.
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains(ErrorCodes.B2CForgottenPassword))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.Failure is OpenIdConnectProtocolException can be evaluated once rather than re-evaluated in multiple if statements.

}

return _userFlowToIssuerAddress[userFlow];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this will be twice as fast:

Suggested change
}
if (!_userFlowToIssuerAddress.TryGetValue(userFlow, out var issuerAddress))
{
issuerAddress = context.ProtocolMessage.IssuerAddress.ToLowerInvariant()
.Replace($"/{defaultUserFlow?.ToLowerInvariant()}/", $"/{userFlow.ToLowerInvariant()}/");
_userFlowToIssuerAddress[userFlow] = issuerAddress;
}
return issuerAddress;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol...i had fixed that when doing the ContainsKey fixes.

{
return (false, new UnauthorizedObjectResult(new ProblemDetails
{
Title = "Authorization failed.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title string needs to be localized.


private static readonly string DoubleBase64PadCharacter = string.Format(CultureInfo.InvariantCulture, "{0}{0}", Base64PadCharacter);
private static readonly string DoubleBase64PadCharacter = string.Format(CultureInfo.InvariantCulture, "{0}{0}", Base64PadCharacter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good example of where string.Format isn't needed

Suggested change
private static readonly string DoubleBase64PadCharacter = string.Format(CultureInfo.InvariantCulture, "{0}{0}", Base64PadCharacter);
private const string DoubleBase64PadCharacter = Base64PadCharacter + Base64PadCharacter;

/// User assigned managed identity client ID (as opposed to system assigned managed identity)
/// See https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-manage-ua-identity-portal.
/// </summary>
public static string? UserAssignedManagedIdentityClientId { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this be static and externally settable opens up race conditions where someone thinks it is populated when it may not be, and then makes incorrect decisions based upon that. Saying it is not used that way now is not sufficient. This is a high risk of misuse for future contributors. Recommendation is to make it an instance field on a new interface that is not settable externally, then access it through dependency injection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will raise an issue for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// subscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);
/// HttpContext.User = ClaimsPrincipalExtension.FromTenantIdAndObjectId(subscription.TenantId,
/// subscription.UserId);
/// string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sample looks a bit broken with missing end brackets and overwriting of HttpContext.User with each iteration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great eye.

internal class ClientInfo
{
[JsonPropertyName(ClaimConstants.UniqueObjectIdentifier)]
public string UniqueObjectIdentifier { get; set; } = null!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting to null! seems like we are incorrectly trying to override the type. The reality is that json can omit the field which would result in a null string.

/// <returns>User flow ID of the identity, or <c>null</c> if it cannot be found.</returns>
public static string? GetUserFlowId(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these extension methods could be deduplicated into calling a single private shared method:

private static string? GetClaimValue(ClaimsPrincipal? claimsPrincipal, params string[] claimNames)
{
    if (claimsPrincipal == null)
    {
        throw new ArgumentNullException(nameof(claimsPrincipal));
    }

    for (var i = 0; i < claimNames.Length; i++)
    {
        var currentValue = claimsPrincipal.FindFirstValue(claimNames[i]);
        if (!string.IsNullOrEmpty(currentValue))
        {
            return currentValue;
        }
    }

    return null;
}

/// <summary>
/// General constants for Microsoft Identity Web.
/// </summary>
public static class Constants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many constants throughout the codebase. It may be worthwhile to rename this class to be more descriptive about the specific constants it holds.

public const string SerializingSessionCache = "Serializing session {0}, cache key {1}. ";
public const string ClearingSessionCache = "Clearing session {0}, cache key {1}. ";
// Caching
public const string DeserializingSessionCache = "Deserializing session {0}, cache key {1}. ";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider using LoggerMessage as described at https://docs.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging instead of doing costly message formats into strings.


bool IsIosVersion(int major)
{
string regex = @"\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is in the hotpath of requests. As such, it would likely speed up by 5-7x if regular expressions were avoided where possible. We did a similar change in a planet scale service and sped the code up by 7x.

{
_tokenAcquisition = tokenAcquisition;
_namedDownstreamWebApiOptions = namedDownstreamWebApiOptions;
_httpClient = httpClient;
#pragma warning disable CA1062 // Validate arguments of public methods
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning suppression might be able to be removed now that strict null types is enabled.

httpRequestMessage.Headers.Add(
Constants.Authorization,
authResult.CreateAuthorizationHeader());
response = await _httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the response var isn't used outside the scope of the using block. It can be removed and replaced by a direct return statement in front of the awaited call.

user,
new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, "application/json")).ConfigureAwait(false);

try
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exceptions for control flow is a very inefficient way of doing things. I recommend instead checking if (!response.IsSuccessStatusCode) and only then throwing the HttpRequestException.

}
private static StringContent ConvertFromInput<TInput>(TInput input)
{
return new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, "application/json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the input is intended to be default json is something which disallows calling downstream apis using other formats such as msgpack. Msgpack is a popular format used within internal containerized servers that need more performance when communicating amongst each other.

/// Options passed-in to call downstream web APIs. To call Microsoft Graph, see rather
/// <c>MicrosoftGraphOptions</c> in the <c>Microsoft.Identity.Web.MicrosoftGraph</c> assembly.
/// </summary>
public class DownstreamWebApiOptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clonable, so why not have it implement ICloneable?

/// <code>
/// public async Task&lt;IEnumerable&lt;MyItem&gt;&gt; GetAsync()
/// {
/// return await _downstreamWebApi.CallWebApiForUserAsync&lt;object, IEnumerable&lt;MyItem&gt;&gt;(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to illustrate best practices, the task should be returned instead of awaited in the example.

/// <c>true</c> if the specified string collection contains any; otherwise, <c>false</c>.</returns>
public static bool ContainsAny(this string searchFor, params string[] stringCollection)
{
foreach (string str in stringCollection)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done faster, but probably shouldn't be. The faster implementation I'm thinking of involves char operations and a trie built from the string collection inputs..

#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
string fullVersion = typeof(IdHelper).GetTypeInfo().Assembly.FullName;
string fullVersion = typeof(IdHelper).GetTypeInfo().Assembly.FullName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the FullName field parsed rather than using the .Version property of the Assembly object?

scopes.Union(additionalBuiltInScopes).ToList());
properties.SetParameter<ICollection<string>>(
OpenIdConnectParameterNames.Scope,
scopes.Union(additionalBuiltInScopes).ToList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LINQ is super slow. Like, so slow, I would recommend people almost never use it when there is a non-verbose alternative. In this case, there is a data structure that perfectly does what we want in an optimized way. That data structure is the HashSet which happens to have a UnionWith method and already implements ICollection.

/// <summary>
/// Handler for Blazor specific APIs to handle incremental consent
/// and conditional access.
/// </summary>
#pragma warning disable SA1402 // File may only contain a single type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the type be moved to its own file in order to make it easier to find on the github folder view?

#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case
(!IsBlazorServer ? CreateBaseUri(_httpContextAccessor.HttpContext.Request) :
(!IsBlazorServer ? CreateBaseUri(_httpContextAccessor.HttpContext.Request) :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everytime HttpContext is touched, it will need somesort of lock in this code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
else
{
throw exception;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is a problem because it causes the original stacktrace to be lost. The exception needs to be thrown with throw; instead of throw exception; which means it can't be done inside this method and instead needs to be done from a catch block directly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are given the exception to process here for incremental consent, not sure how we can move this to a try/catch. otherwise, developers would have to do this, and it becomes too complicated.


effectiveScopes = effectiveScopes.Union(additionalBuiltInScopes).ToArray();
effectiveScopes = effectiveScopes.Union(additionalBuiltInScopes).ToArray();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToArray results in an unnecessary allocation.

redirectUri = NavigationManager.Uri;
}
else
{
#pragma warning disable CS8602 // Dereference of a possibly null reference. HttpContext will not be null in this case.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpContext can definitely be null here if the library is attempted to be used within a worker service. With that in mind, the warning should be addressed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only for blazorwasm scenarios, so we will always have the httpContext.

{
var state = await Provider.GetAuthenticationStateAsync().ConfigureAwait(false);
Service.User = state.User;
Service.IsBlazorServer = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsBlazorServer might not be correct or am I reading this wrong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only for blazorwasm...it's a long story.

IsValidTidInLocalPath(tenantId, actualIssuerUri);
}
catch
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

faults should minimally be logged.

jwtSecurityToken.Payload.TryGetValue(ClaimConstants.TenantId, out object? tenantId);
if (tenantId != null)
{
return (string)tenantId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case someone is trying to break things, the cast would be safer as tenantId as string;

/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>
private Func<AuthenticationFailedContext, Task> s_onAuthenticationFailed = null!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This naming convention doesn't match the rest of the codebase. Historically, s_ meant static, which makes this naming a bit misleading.

.Distinct();
_issuerValidators[authorityHost] = new AadIssuerValidator(aliases);
// Add issuer aliases of the chosen authority to the cache
IEnumerable<string> aliases = issuerMetadata.Metadata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use the unLINQ treatment.

_logger.LogDebug(string.Format(CultureInfo.InvariantCulture, LogMessages.MethodEnd, nameof(OnRedirectToIdentityProviderAsync)));
}

private void DisplayProtocolMessage(OpenIdConnectMessage message)
Copy link
Contributor

@DavidParks8 DavidParks8 Mar 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this please be ifdef'd out of existence do it is excluded from the build in release mode? The reflection used in this method makes it very slow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

developers have to set it explicitly, and i doubt they would use it in release mode. we can make a comment in the wiki, and we could add an if/def too...sometimes we might need the diagnostics in release mode.

}
private void ValidateEffectiveScopes(AuthorizationFilterContext context)
{
if (_effectiveAcceptedScopes == null || !_effectiveAcceptedScopes.Any())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.Any() is much slower than string[].Length > 0;

string.Join(",", _effectiveAcceptedScopes));
context.HttpContext.Response.WriteAsync(message);
context.HttpContext.Response.CompleteAsync();
throw new UnauthorizedAccessException(message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does an exception need to be thrown when the response was already completed in the line above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the rest of the controller action is not done. if the scope is not correct, we want to avoid the rest of the controller action from happening. there might be a better way to handle it, will take a look. if you have any suggestions, let us know.

/// <param name="acceptedRoles">Roles accepted by this web API.</param>
/// <remarks>When the roles don't match, the response is a 403 (Forbidden),
/// because the app does not have the expected roles.</remarks>
public static void ValidateAppRole(this HttpContext context, params string[] acceptedRoles)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httpContext needs locking to avoid concurrent reads with external code.


if (result == null)
{
var measure = await Task.Run(async () =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Task.Run is usually something that shouldn't be done in an already async function for perf/context switching reasons. It looks like it is only done to measure things. I suggest removing task.run and measuring with StopWatch.StartNew.


private readonly ReaderWriterLockSlim _sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
}
private readonly ReaderWriterLockSlim _sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReaderWriterLockSlim is disposable. Thus MsalSessionTokenCacheProvider should also be disposable.

var tokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
if (!microsoftIdentityOptions.AllowWebApiToBeAuthorizedByACL && !context!.Principal!.Claims.Any(x => x.Type == ClaimConstants.Scope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than iterating the claims 4 times, it can all be done in one .Any call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants