diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs
index 5e1228593a87..630fd059199c 100644
--- a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs
+++ b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs
@@ -40,7 +40,7 @@ public interface IAuthenticationService
Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties);
///
- /// Sign a principal in for the specified authentication scheme.
+ /// Sign in a principal in for the specified authentication scheme.
///
/// The .
/// The name of the authentication scheme.
diff --git a/src/Http/Authentication.Core/src/AuthenticationService.cs b/src/Http/Authentication.Core/src/AuthenticationService.cs
index 3b45ffc56dc6..dec4279a7c56 100644
--- a/src/Http/Authentication.Core/src/AuthenticationService.cs
+++ b/src/Http/Authentication.Core/src/AuthenticationService.cs
@@ -145,7 +145,7 @@ public virtual async Task ForbidAsync(HttpContext context, string? scheme, Authe
}
///
- /// Sign a principal in for the specified authentication scheme.
+ /// Sign in a principal in for the specified authentication scheme.
///
/// The .
/// The name of the authentication scheme.
diff --git a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
index f3faa30a2af4..503a2ab0e90e 100644
--- a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
+++ b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
@@ -13,7 +13,8 @@
-
+
+
diff --git a/src/Identity/Core/src/SignInManager.cs b/src/Identity/Core/src/SignInManager.cs
index 66f06c4d3465..18b7c6a5dd48 100644
--- a/src/Identity/Core/src/SignInManager.cs
+++ b/src/Identity/Core/src/SignInManager.cs
@@ -7,6 +7,7 @@
using System.Text;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -24,6 +25,7 @@ public class SignInManager where TUser : class
private readonly IHttpContextAccessor _contextAccessor;
private readonly IAuthenticationSchemeProvider _schemes;
private readonly IUserConfirmation _confirmation;
+ private readonly SignInManagerMetrics? _metrics;
private HttpContext? _context;
private TwoFactorAuthenticationInfo? _twoFactorInfo;
@@ -56,6 +58,7 @@ public SignInManager(UserManager userManager,
Logger = logger;
_schemes = schemes;
_confirmation = confirmation;
+ _metrics = userManager.ServiceProvider?.GetService();
}
///
@@ -160,12 +163,26 @@ public virtual async Task CanSignInAsync(TUser user)
/// The user to sign-in.
/// The task object representing the asynchronous operation.
public virtual async Task RefreshSignInAsync(TUser user)
+ {
+ try
+ {
+ var (success, isPersistent) = await RefreshSignInCoreAsync(user);
+ _metrics?.RefreshSignIn(typeof(TUser).FullName!, AuthenticationScheme, success, isPersistent);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.RefreshSignIn(typeof(TUser).FullName!, AuthenticationScheme, success: null, isPersistent: null, ex);
+ throw;
+ }
+ }
+
+ private async Task<(bool success, bool? isPersistent)> RefreshSignInCoreAsync(TUser user)
{
var auth = await Context.AuthenticateAsync(AuthenticationScheme);
if (!auth.Succeeded || auth.Principal?.Identity?.IsAuthenticated != true)
{
Logger.LogError("RefreshSignInAsync prevented because the user is not currently authenticated. Use SignInAsync instead for initial sign in.");
- return;
+ return (false, auth.Properties?.IsPersistent);
}
var authenticatedUserId = UserManager.GetUserId(auth.Principal);
@@ -173,12 +190,12 @@ public virtual async Task RefreshSignInAsync(TUser user)
if (authenticatedUserId == null || authenticatedUserId != newUserId)
{
Logger.LogError("RefreshSignInAsync prevented because currently authenticated user has a different UserId. Use SignInAsync instead to change users.");
- return;
+ return (false, auth.Properties?.IsPersistent);
}
IList claims = Array.Empty();
- var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod);
- var amr = auth?.Principal?.FindFirst("amr");
+ var authenticationMethod = auth.Principal?.FindFirst(ClaimTypes.AuthenticationMethod);
+ var amr = auth.Principal?.FindFirst("amr");
if (authenticationMethod != null || amr != null)
{
@@ -193,7 +210,8 @@ public virtual async Task RefreshSignInAsync(TUser user)
}
}
- await SignInWithClaimsAsync(user, auth?.Properties, claims);
+ await SignInWithClaimsAsync(user, auth.Properties, claims);
+ return (true, auth.Properties?.IsPersistent ?? false);
}
///
@@ -245,17 +263,27 @@ public virtual Task SignInWithClaimsAsync(TUser user, bool isPersistent, IEnumer
/// The task object representing the asynchronous operation.
public virtual async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties? authenticationProperties, IEnumerable additionalClaims)
{
- var userPrincipal = await CreateUserPrincipalAsync(user);
- foreach (var claim in additionalClaims)
+ try
{
- userPrincipal.Identities.First().AddClaim(claim);
- }
- await Context.SignInAsync(AuthenticationScheme,
- userPrincipal,
- authenticationProperties ?? new AuthenticationProperties());
+ var userPrincipal = await CreateUserPrincipalAsync(user);
+ foreach (var claim in additionalClaims)
+ {
+ userPrincipal.Identities.First().AddClaim(claim);
+ }
+ await Context.SignInAsync(AuthenticationScheme,
+ userPrincipal,
+ authenticationProperties ?? new AuthenticationProperties());
- // This is useful for updating claims immediately when hitting MapIdentityApi's /account/info endpoint with cookies.
- Context.User = userPrincipal;
+ // This is useful for updating claims immediately when hitting MapIdentityApi's /account/info endpoint with cookies.
+ Context.User = userPrincipal;
+
+ _metrics?.SignInUserPrincipal(typeof(TUser).FullName!, AuthenticationScheme);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.SignInUserPrincipal(typeof(TUser).FullName!, AuthenticationScheme, ex);
+ throw;
+ }
}
///
@@ -263,15 +291,25 @@ await Context.SignInAsync(AuthenticationScheme,
///
public virtual async Task SignOutAsync()
{
- await Context.SignOutAsync(AuthenticationScheme);
-
- if (await _schemes.GetSchemeAsync(IdentityConstants.ExternalScheme) != null)
+ try
{
- await Context.SignOutAsync(IdentityConstants.ExternalScheme);
+ await Context.SignOutAsync(AuthenticationScheme);
+
+ if (await _schemes.GetSchemeAsync(IdentityConstants.ExternalScheme) != null)
+ {
+ await Context.SignOutAsync(IdentityConstants.ExternalScheme);
+ }
+ if (await _schemes.GetSchemeAsync(IdentityConstants.TwoFactorUserIdScheme) != null)
+ {
+ await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
+ }
+
+ _metrics?.SignOutUserPrincipal(typeof(TUser).FullName!, AuthenticationScheme);
}
- if (await _schemes.GetSchemeAsync(IdentityConstants.TwoFactorUserIdScheme) != null)
+ catch (Exception ex)
{
- await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
+ _metrics?.SignOutUserPrincipal(typeof(TUser).FullName!, AuthenticationScheme, ex);
+ throw;
}
}
@@ -345,12 +383,23 @@ public virtual async Task ValidateSecurityStampAsync(TUser? user, string?
public virtual async Task PasswordSignInAsync(TUser user, string password,
bool isPersistent, bool lockoutOnFailure)
{
- ArgumentNullException.ThrowIfNull(user);
+ try
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
+ var result = attempt.Succeeded
+ ? await SignInOrTwoFactorAsync(user, isPersistent)
+ : attempt;
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result, SignInType.Password, isPersistent);
- var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
- return attempt.Succeeded
- ? await SignInOrTwoFactorAsync(user, isPersistent)
- : attempt;
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result: null, SignInType.Password, isPersistent, ex);
+ throw;
+ }
}
///
@@ -369,6 +418,7 @@ public virtual async Task PasswordSignInAsync(string userName, str
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, SignInResult.Failed, SignInType.Password, isPersistent);
return SignInResult.Failed;
}
@@ -386,8 +436,24 @@ public virtual async Task PasswordSignInAsync(string userName, str
///
public virtual async Task CheckPasswordSignInAsync(TUser user, string password, bool lockoutOnFailure)
{
- ArgumentNullException.ThrowIfNull(user);
+ try
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ var result = await CheckPasswordSignInCoreAsync(user, password, lockoutOnFailure);
+ _metrics?.CheckPasswordSignIn(typeof(TUser).FullName!, result);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.CheckPasswordSignIn(typeof(TUser).FullName!, result: null, ex);
+ throw;
+ }
+ }
+
+ private async Task CheckPasswordSignInCoreAsync(TUser user, string password, bool lockoutOnFailure)
+ {
var error = await PreSignInCheck(user);
if (error != null)
{
@@ -461,19 +527,37 @@ public virtual async Task IsTwoFactorClientRememberedAsync(TUser user)
/// The task object representing the asynchronous operation.
public virtual async Task RememberTwoFactorClientAsync(TUser user)
{
- var principal = await StoreRememberClient(user);
- await Context.SignInAsync(IdentityConstants.TwoFactorRememberMeScheme,
- principal,
- new AuthenticationProperties { IsPersistent = true });
+ try
+ {
+ var principal = await StoreRememberClient(user);
+ await Context.SignInAsync(IdentityConstants.TwoFactorRememberMeScheme,
+ principal,
+ new AuthenticationProperties { IsPersistent = true });
+ _metrics?.RememberTwoFactorClient(typeof(TUser).FullName!, IdentityConstants.TwoFactorRememberMeScheme);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.RememberTwoFactorClient(typeof(TUser).FullName!, IdentityConstants.TwoFactorRememberMeScheme, ex);
+ throw;
+ }
}
///
/// Clears the "Remember this browser flag" from the current browser, as an asynchronous operation.
///
/// The task object representing the asynchronous operation.
- public virtual Task ForgetTwoFactorClientAsync()
+ public virtual async Task ForgetTwoFactorClientAsync()
{
- return Context.SignOutAsync(IdentityConstants.TwoFactorRememberMeScheme);
+ try
+ {
+ await Context.SignOutAsync(IdentityConstants.TwoFactorRememberMeScheme);
+ _metrics?.ForgetTwoFactorClient(typeof(TUser).FullName!, IdentityConstants.TwoFactorRememberMeScheme);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.ForgetTwoFactorClient(typeof(TUser).FullName!, IdentityConstants.TwoFactorRememberMeScheme, ex);
+ throw;
+ }
}
///
@@ -482,6 +566,22 @@ public virtual Task ForgetTwoFactorClientAsync()
/// The two factor recovery code.
///
public virtual async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
+ {
+ try
+ {
+ var result = await TwoFactorRecoveryCodeSignInCoreAsync(recoveryCode);
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result, SignInType.TwoFactorRecoveryCode, isPersistent: false);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result: null, SignInType.TwoFactorRecoveryCode, isPersistent: false, ex);
+ throw;
+ }
+ }
+
+ private async Task TwoFactorRecoveryCodeSignInCoreAsync(string recoveryCode)
{
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
if (twoFactorInfo == null)
@@ -510,8 +610,10 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAut
return SignInResult.Failed;
}
- var claims = new List();
- claims.Add(new Claim("amr", "mfa"));
+ var claims = new List
+ {
+ new Claim("amr", "mfa")
+ };
if (twoFactorInfo.LoginProvider != null)
{
@@ -545,6 +647,22 @@ private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAut
/// The task object representing the asynchronous operation containing the
/// for the sign-in attempt.
public virtual async Task TwoFactorAuthenticatorSignInAsync(string code, bool isPersistent, bool rememberClient)
+ {
+ try
+ {
+ var result = await TwoFactorAuthenticatorSignInCoreAsync(code, isPersistent, rememberClient);
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result, SignInType.TwoFactorAuthenticator, isPersistent);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result: null, SignInType.TwoFactorAuthenticator, isPersistent, ex);
+ throw;
+ }
+ }
+
+ private async Task TwoFactorAuthenticatorSignInCoreAsync(string code, bool isPersistent, bool rememberClient)
{
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
if (twoFactorInfo == null)
@@ -593,6 +711,22 @@ public virtual async Task TwoFactorAuthenticatorSignInAsync(string
/// The task object representing the asynchronous operation containing the
/// for the sign-in attempt.
public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient)
+ {
+ try
+ {
+ var result = await TwoFactorSignInCoreAsync(provider, code, isPersistent, rememberClient);
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result, SignInType.TwoFactor, isPersistent);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result: null, SignInType.TwoFactor, isPersistent, ex);
+ throw;
+ }
+ }
+
+ private async Task TwoFactorSignInCoreAsync(string provider, string code, bool isPersistent, bool rememberClient)
{
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
if (twoFactorInfo == null)
@@ -666,6 +800,22 @@ public virtual Task ExternalLoginSignInAsync(string loginProvider,
/// The task object representing the asynchronous operation containing the
/// for the sign-in attempt.
public virtual async Task ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
+ {
+ try
+ {
+ var result = await ExternalLoginSignInCoreAsync(loginProvider, providerKey, isPersistent, bypassTwoFactor);
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result, SignInType.External, isPersistent);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.AuthenticateSignIn(typeof(TUser).FullName!, AuthenticationScheme, result: null, SignInType.External, isPersistent, ex);
+ throw;
+ }
+ }
+
+ private async Task ExternalLoginSignInCoreAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
var user = await UserManager.FindByLoginAsync(loginProvider, providerKey);
if (user == null)
diff --git a/src/Identity/Core/src/SignInManagerMetrics.cs b/src/Identity/Core/src/SignInManagerMetrics.cs
new file mode 100644
index 000000000000..d4b82dfcf015
--- /dev/null
+++ b/src/Identity/Core/src/SignInManagerMetrics.cs
@@ -0,0 +1,192 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+
+namespace Microsoft.AspNetCore.Identity;
+
+internal sealed class SignInManagerMetrics : IDisposable
+{
+ public const string MeterName = "Microsoft.AspNetCore.Identity";
+
+ public const string AuthenticateCounterName = "aspnetcore.identity.sign_in.authenticate";
+ public const string RememberTwoFactorCounterName = "aspnetcore.identity.sign_in.remember_two_factor";
+ public const string ForgetTwoFactorCounterName = "aspnetcore.identity.sign_in.forget_two_factor";
+ public const string RefreshCounterName = "aspnetcore.identity.sign_in.refresh";
+ public const string CheckPasswordCounterName = "aspnetcore.identity.sign_in.check_password";
+ public const string SignInUserPrincipalCounterName = "aspnetcore.identity.sign_in.sign_in_principal";
+ public const string SignOutUserPrincipalCounterName = "aspnetcore.identity.sign_in.sign_out_principal";
+
+ private readonly Meter _meter;
+ private readonly Counter _authenticateCounter;
+ private readonly Counter _rememberTwoFactorClientCounter;
+ private readonly Counter _forgetTwoFactorCounter;
+ private readonly Counter _refreshCounter;
+ private readonly Counter _checkPasswordCounter;
+ private readonly Counter _signInUserPrincipalCounter;
+ private readonly Counter _signOutUserPrincipalCounter;
+
+ public SignInManagerMetrics(IMeterFactory meterFactory)
+ {
+ _meter = meterFactory.Create(MeterName);
+
+ _authenticateCounter = _meter.CreateCounter(AuthenticateCounterName, "count", "The number of authenticate and sign in attempts.");
+ _rememberTwoFactorClientCounter = _meter.CreateCounter(RememberTwoFactorCounterName, "count", "The number of two factor clients remembered.");
+ _forgetTwoFactorCounter = _meter.CreateCounter(ForgetTwoFactorCounterName, "count", "The number of two factor clients forgotten.");
+ _refreshCounter = _meter.CreateCounter(RefreshCounterName, "count", "The number of refresh sign-in attempts.");
+ _checkPasswordCounter = _meter.CreateCounter(CheckPasswordCounterName, "count", "The number of check password attempts.");
+ _signInUserPrincipalCounter = _meter.CreateCounter(SignInUserPrincipalCounterName, "count", "The number of user principals signed in.");
+ _signOutUserPrincipalCounter = _meter.CreateCounter(SignOutUserPrincipalCounterName, "count", "The number of user principals signed out.");
+ }
+
+ internal void CheckPasswordSignIn(string userType, SignInResult? result, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ };
+ AddSignInResult(ref tags, result);
+ AddExceptionTags(ref tags, exception);
+
+ _checkPasswordCounter.Add(1, tags);
+ }
+
+ internal void AuthenticateSignIn(string userType, string authenticationScheme, SignInResult? result, SignInType signInType, bool isPersistent, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme },
+ { "aspnetcore.identity.sign_in.type", GetSignInType(signInType) },
+ { "aspnetcore.identity.sign_in.is_persistent", isPersistent },
+ };
+ if (result != null)
+ {
+ tags.Add("aspnetcore.identity.sign_in.result", GetSignInResult(result));
+ }
+ AddExceptionTags(ref tags, exception);
+
+ _authenticateCounter.Add(1, tags);
+ }
+
+ internal void SignInUserPrincipal(string userType, string authenticationScheme, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme },
+ };
+ AddExceptionTags(ref tags, exception);
+
+ _signInUserPrincipalCounter.Add(1, tags);
+ }
+
+ internal void SignOutUserPrincipal(string userType, string authenticationScheme, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme },
+ };
+ AddExceptionTags(ref tags, exception);
+
+ _signOutUserPrincipalCounter.Add(1, tags);
+ }
+
+ internal void RememberTwoFactorClient(string userType, string authenticationScheme, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme }
+ };
+ AddExceptionTags(ref tags, exception);
+
+ _rememberTwoFactorClientCounter.Add(1, tags);
+ }
+
+ internal void ForgetTwoFactorClient(string userType, string authenticationScheme, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme }
+ };
+ AddExceptionTags(ref tags, exception);
+
+ _forgetTwoFactorCounter.Add(1, tags);
+ }
+
+ internal void RefreshSignIn(string userType, string authenticationScheme, bool? success, bool? isPersistent, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.authentication_scheme", authenticationScheme },
+ { "aspnetcore.identity.sign_in.result", success.GetValueOrDefault() ? "success" : "failure" }
+ };
+ if (isPersistent != null)
+ {
+ tags.Add("aspnetcore.identity.sign_in.is_persistent", isPersistent.Value);
+ }
+ AddExceptionTags(ref tags, exception);
+
+ _refreshCounter.Add(1, tags);
+ }
+
+ public void Dispose()
+ {
+ _meter.Dispose();
+ }
+
+ private static void AddSignInResult(ref TagList tags, SignInResult? result)
+ {
+ if (result != null)
+ {
+ tags.Add("aspnetcore.identity.sign_in.result", GetSignInResult(result));
+ }
+ }
+
+ private static void AddExceptionTags(ref TagList tags, Exception? exception)
+ {
+ if (exception != null)
+ {
+ tags.Add("error.type", exception.GetType().FullName!);
+ }
+ }
+
+ private static string GetSignInType(SignInType signInType)
+ {
+ return signInType switch
+ {
+ SignInType.Password => "password",
+ SignInType.TwoFactorRecoveryCode => "two_factor_recovery_code",
+ SignInType.TwoFactorAuthenticator => "two_factor_authenticator",
+ SignInType.TwoFactor => "two_factor",
+ SignInType.External => "external",
+ _ => "_UNKNOWN"
+ };
+ }
+
+ private static string GetSignInResult(SignInResult result)
+ {
+ return result switch
+ {
+ { Succeeded: true } => "success",
+ { IsLockedOut: true } => "locked_out",
+ { IsNotAllowed: true } => "not_allowed",
+ { RequiresTwoFactor: true } => "requires_two_factor",
+ _ => "failure"
+ };
+ }
+}
+
+internal enum SignInType
+{
+ Password,
+ TwoFactorRecoveryCode,
+ TwoFactorAuthenticator,
+ TwoFactor,
+ External
+}
diff --git a/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt b/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
index 58c861652420..e24860e08367 100644
--- a/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
+++ b/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,4 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Identity.UserLoginInfo.UserLoginInfo(string! loginProvider, string! providerKey, string? displayName) -> void
Microsoft.AspNetCore.Identity.UserLoginInfo.UserLoginInfo(string! loginProvider, string! providerKey, string? providerDisplayName) -> void
+Microsoft.AspNetCore.Identity.UserManager.ServiceProvider.get -> System.IServiceProvider!
diff --git a/src/Identity/Extensions.Core/src/UserManager.cs b/src/Identity/Extensions.Core/src/UserManager.cs
index cca2005d10d0..94a292d02176 100644
--- a/src/Identity/Extensions.Core/src/UserManager.cs
+++ b/src/Identity/Extensions.Core/src/UserManager.cs
@@ -47,7 +47,7 @@ public class UserManager : IDisposable where TUser : class
#if NETSTANDARD2_0 || NETFRAMEWORK
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
#endif
- private readonly IServiceProvider _services;
+ private readonly UserManagerMetrics? _metrics;
///
/// The cancellation token used to cancel operations.
@@ -83,6 +83,8 @@ public UserManager(IUserStore store,
KeyNormalizer = keyNormalizer;
ErrorDescriber = errors;
Logger = logger;
+ ServiceProvider = services;
+ _metrics = services?.GetService();
if (userValidators != null)
{
@@ -99,7 +101,6 @@ public UserManager(IUserStore store,
}
}
- _services = services;
if (services != null)
{
foreach (var providerName in Options.Tokens.ProviderMap.Keys)
@@ -176,6 +177,11 @@ public UserManager(IUserStore store,
///
public IdentityOptions Options { get; set; }
+ ///
+ /// The used to resolve Identity services.
+ ///
+ public IServiceProvider ServiceProvider { get; }
+
///
/// Gets a flag indicating whether the backing user store supports authentication tokens.
///
@@ -459,13 +465,28 @@ public virtual Task GenerateConcurrencyStampAsync(TUser user)
/// of the operation.
///
public virtual async Task CreateAsync(TUser user)
+ {
+ try
+ {
+ var result = await CreateCoreAsync(user).ConfigureAwait(false);
+ _metrics?.CreateUser(typeof(TUser).FullName!, result);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.CreateUser(typeof(TUser).FullName!, result: null, ex);
+ throw;
+ }
+ }
+
+ private async Task CreateCoreAsync(TUser user)
{
ThrowIfDisposed();
await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- var result = await ValidateUserAsync(user).ConfigureAwait(false);
- if (!result.Succeeded)
+ var validateUserResult = await ValidateUserAsync(user).ConfigureAwait(false);
+ if (!validateUserResult.Succeeded)
{
- return result;
+ return validateUserResult;
}
if (Options.Lockout.AllowedForNewUsers && SupportsUserLockout)
{
@@ -485,12 +506,20 @@ public virtual async Task CreateAsync(TUser user)
/// The that represents the asynchronous operation, containing the
/// of the operation.
///
- public virtual Task UpdateAsync(TUser user)
+ public virtual async Task UpdateAsync(TUser user)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- return UpdateUserAsync(user);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.Update).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.Update, ex);
+ throw;
+ }
}
///
@@ -501,12 +530,23 @@ public virtual Task UpdateAsync(TUser user)
/// The that represents the asynchronous operation, containing the
/// of the operation.
///
- public virtual Task DeleteAsync(TUser user)
+ public virtual async Task DeleteAsync(TUser user)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+
+ var result = await Store.DeleteAsync(user, CancellationToken).ConfigureAwait(false);
+ _metrics?.DeleteUser(typeof(TUser).FullName!, result);
- return Store.DeleteAsync(user, CancellationToken);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.DeleteUser(typeof(TUser).FullName!, result: null, ex);
+ throw;
+ }
}
///
@@ -540,8 +580,8 @@ public virtual Task DeleteAsync(TUser user)
// Need to potentially check all keys
if (user == null && Options.Stores.ProtectPersonalData)
{
- var keyRing = _services.GetService();
- var protector = _services.GetService();
+ var keyRing = ServiceProvider.GetService();
+ var protector = ServiceProvider.GetService();
if (keyRing != null && protector != null)
{
foreach (var key in keyRing.GetAllKeyIds())
@@ -570,15 +610,26 @@ public virtual Task DeleteAsync(TUser user)
///
public virtual async Task CreateAsync(TUser user, string password)
{
- ThrowIfDisposed();
- var passwordStore = GetPasswordStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(password);
- var result = await UpdatePasswordHash(passwordStore, user, password).ConfigureAwait(false);
- if (!result.Succeeded)
+ try
{
- return result;
+ ThrowIfDisposed();
+ var passwordStore = GetPasswordStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(password);
+ var result = await UpdatePasswordHash(passwordStore, user, password).ConfigureAwait(false);
+ if (!result.Succeeded)
+ {
+ _metrics?.CreateUser(typeof(TUser).FullName!, result);
+ return result;
+ }
}
+ catch (Exception ex)
+ {
+ _metrics?.CreateUser(typeof(TUser).FullName!, result: null, ex);
+ throw;
+ }
+
+ // Already has a try/catch.
return await CreateAsync(user).ConfigureAwait(false);
}
@@ -605,8 +656,8 @@ public virtual async Task CreateAsync(TUser user, string passwor
{
if (Options.Stores.ProtectPersonalData)
{
- var keyRing = _services.GetRequiredService();
- var protector = _services.GetRequiredService();
+ var keyRing = ServiceProvider.GetRequiredService();
+ var protector = ServiceProvider.GetRequiredService();
return protector.Protect(keyRing.CurrentKeyId, data);
}
return data;
@@ -644,12 +695,20 @@ public virtual async Task UpdateNormalizedUserNameAsync(TUser user)
/// The that represents the asynchronous operation.
public virtual async Task SetUserNameAsync(TUser user, string? userName)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await Store.SetUserNameAsync(user, userName, CancellationToken).ConfigureAwait(false);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await Store.SetUserNameAsync(user, userName, CancellationToken).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.UserName).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.UserName, ex);
+ throw;
+ }
}
///
@@ -673,27 +732,45 @@ public virtual async Task GetUserIdAsync(TUser user)
/// the specified matches the one store for the ,
/// otherwise false.
public virtual async Task CheckPasswordAsync(TUser user, string password)
+ {
+ try
+ {
+ var (result, userMissing) = await CheckPasswordCoreAsync(user, password).ConfigureAwait(false);
+ _metrics?.CheckPassword(typeof(TUser).FullName!, userMissing, result);
+
+ if (result == PasswordVerificationResult.Failed)
+ {
+ Logger.LogDebug(LoggerEventIds.InvalidPassword, "Invalid password for user.");
+ return false;
+ }
+
+ return result != null;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.CheckPassword(typeof(TUser).FullName!, userMissing: null, result: null, ex);
+ throw;
+ }
+ }
+
+ private async Task<(PasswordVerificationResult? result, bool userMissing)> CheckPasswordCoreAsync(TUser user, string password)
{
ThrowIfDisposed();
var passwordStore = GetPasswordStore();
if (user == null)
{
- return false;
+ return (null, true);
}
var result = await VerifyPasswordAsync(passwordStore, user, password).ConfigureAwait(false);
+
if (result == PasswordVerificationResult.SuccessRehashNeeded)
{
await UpdatePasswordHash(passwordStore, user, password, validatePassword: false).ConfigureAwait(false);
- await UpdateUserAsync(user).ConfigureAwait(false);
+ await UpdateUserAndRecordMetricAsync(user, UserUpdateType.PasswordRehash).ConfigureAwait(false);
}
- var success = result != PasswordVerificationResult.Failed;
- if (!success)
- {
- Logger.LogDebug(LoggerEventIds.InvalidPassword, "Invalid password for user.");
- }
- return success;
+ return (result, false);
}
///
@@ -724,6 +801,21 @@ public virtual Task HasPasswordAsync(TUser user)
/// of the operation.
///
public virtual async Task AddPasswordAsync(TUser user, string password)
+ {
+ try
+ {
+ var result = await AddPasswordCoreAsync(user, password).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.AddPassword);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AddPassword, ex);
+ throw;
+ }
+ }
+
+ private async Task AddPasswordCoreAsync(TUser user, string password)
{
ThrowIfDisposed();
var passwordStore = GetPasswordStore();
@@ -755,6 +847,21 @@ public virtual async Task AddPasswordAsync(TUser user, string pa
/// of the operation.
///
public virtual async Task ChangePasswordAsync(TUser user, string currentPassword, string newPassword)
+ {
+ try
+ {
+ var result = await ChangePasswordCoreAsync(user, currentPassword, newPassword).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ChangePassword);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ChangePassword, ex);
+ throw;
+ }
+ }
+
+ private async Task ChangePasswordCoreAsync(TUser user, string currentPassword, string newPassword)
{
ThrowIfDisposed();
var passwordStore = GetPasswordStore();
@@ -762,10 +869,10 @@ public virtual async Task ChangePasswordAsync(TUser user, string
if (await VerifyPasswordAsync(passwordStore, user, currentPassword).ConfigureAwait(false) != PasswordVerificationResult.Failed)
{
- var result = await UpdatePasswordHash(passwordStore, user, newPassword).ConfigureAwait(false);
- if (!result.Succeeded)
+ var updateResult = await UpdatePasswordHash(passwordStore, user, newPassword).ConfigureAwait(false);
+ if (!updateResult.Succeeded)
{
- return result;
+ return updateResult;
}
return await UpdateUserAsync(user).ConfigureAwait(false);
}
@@ -783,12 +890,20 @@ public virtual async Task ChangePasswordAsync(TUser user, string
///
public virtual async Task RemovePasswordAsync(TUser user)
{
- ThrowIfDisposed();
- var passwordStore = GetPasswordStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var passwordStore = GetPasswordStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await UpdatePasswordHash(passwordStore, user, null, validatePassword: false).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await UpdatePasswordHash(passwordStore, user, null, validatePassword: false).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.RemovePassword).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RemovePassword, ex);
+ throw;
+ }
}
///
@@ -806,9 +921,13 @@ protected virtual async Task VerifyPasswordAsync(IUs
var hash = await store.GetPasswordHashAsync(user, CancellationToken).ConfigureAwait(false);
if (hash == null)
{
+ _metrics?.VerifyPassword(typeof(TUser).FullName!, passwordMissing: true, result: null);
return PasswordVerificationResult.Failed;
}
- return PasswordHasher.VerifyHashedPassword(user, hash, password);
+ var result = PasswordHasher.VerifyHashedPassword(user, hash, password);
+ _metrics?.VerifyPassword(typeof(TUser).FullName!, passwordMissing: false, result);
+
+ return result;
}
///
@@ -843,12 +962,20 @@ public virtual async Task GetSecurityStampAsync(TUser user)
///
public virtual async Task UpdateSecurityStampAsync(TUser user)
{
- ThrowIfDisposed();
- GetSecurityStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ GetSecurityStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SecurityStamp).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SecurityStamp, ex);
+ throw;
+ }
}
///
@@ -883,14 +1010,18 @@ public virtual async Task ResetPasswordAsync(TUser user, string
// Make sure the token is valid and the stamp matches
if (!await VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, ResetPasswordTokenPurpose, token).ConfigureAwait(false))
{
- return IdentityResult.Failed(ErrorDescriber.InvalidToken());
+ var failureResult = IdentityResult.Failed(ErrorDescriber.InvalidToken());
+ _metrics?.UpdateUser(typeof(TUser).FullName!, failureResult, UserUpdateType.ResetPassword);
+
+ return failureResult;
}
var result = await UpdatePasswordHash(user, newPassword, validatePassword: true).ConfigureAwait(false);
if (!result.Succeeded)
{
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ResetPassword);
return result;
}
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.ResetPassword).ConfigureAwait(false);
}
///
@@ -923,15 +1054,23 @@ public virtual async Task ResetPasswordAsync(TUser user, string
///
public virtual async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey)
{
- ThrowIfDisposed();
- var loginStore = GetLoginStore();
- ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
- ArgumentNullThrowHelper.ThrowIfNull(providerKey);
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var loginStore = GetLoginStore();
+ ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
+ ArgumentNullThrowHelper.ThrowIfNull(providerKey);
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await loginStore.RemoveLoginAsync(user, loginProvider, providerKey, CancellationToken).ConfigureAwait(false);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await loginStore.RemoveLoginAsync(user, loginProvider, providerKey, CancellationToken).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.RemoveLogin).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RemoveLogin, ex);
+ throw;
+ }
}
///
@@ -944,6 +1083,21 @@ public virtual async Task RemoveLoginAsync(TUser user, string lo
/// of the operation.
///
public virtual async Task AddLoginAsync(TUser user, UserLoginInfo login)
+ {
+ try
+ {
+ var result = await AddLoginCoreAsync(user, login).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.AddLogin);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AddLogin, ex);
+ throw;
+ }
+ }
+
+ private async Task AddLoginCoreAsync(TUser user, UserLoginInfo login)
{
ThrowIfDisposed();
var loginStore = GetLoginStore();
@@ -986,11 +1140,7 @@ public virtual async Task> GetLoginsAsync(TUser user)
///
public virtual Task AddClaimAsync(TUser user, Claim claim)
{
- ThrowIfDisposed();
- GetClaimStore();
- ArgumentNullThrowHelper.ThrowIfNull(claim);
- ArgumentNullThrowHelper.ThrowIfNull(user);
- return AddClaimsAsync(user, new Claim[] { claim });
+ return AddClaimsAsync(user, [claim]);
}
///
@@ -1004,13 +1154,21 @@ public virtual Task AddClaimAsync(TUser user, Claim claim)
///
public virtual async Task AddClaimsAsync(TUser user, IEnumerable claims)
{
- ThrowIfDisposed();
- var claimStore = GetClaimStore();
- ArgumentNullThrowHelper.ThrowIfNull(claims);
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var claimStore = GetClaimStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(claims);
- await claimStore.AddClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await claimStore.AddClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.AddClaims).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AddClaims, ex);
+ throw;
+ }
}
///
@@ -1025,14 +1183,22 @@ public virtual async Task AddClaimsAsync(TUser user, IEnumerable
///
public virtual async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim)
{
- ThrowIfDisposed();
- var claimStore = GetClaimStore();
- ArgumentNullThrowHelper.ThrowIfNull(claim);
- ArgumentNullThrowHelper.ThrowIfNull(newClaim);
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var claimStore = GetClaimStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(claim);
+ ArgumentNullThrowHelper.ThrowIfNull(newClaim);
- await claimStore.ReplaceClaimAsync(user, claim, newClaim, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await claimStore.ReplaceClaimAsync(user, claim, newClaim, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.ReplaceClaim).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ReplaceClaim, ex);
+ throw;
+ }
}
///
@@ -1046,11 +1212,7 @@ public virtual async Task ReplaceClaimAsync(TUser user, Claim cl
///
public virtual Task RemoveClaimAsync(TUser user, Claim claim)
{
- ThrowIfDisposed();
- GetClaimStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(claim);
- return RemoveClaimsAsync(user, new Claim[] { claim });
+ return RemoveClaimsAsync(user, [claim]);
}
///
@@ -1064,13 +1226,21 @@ public virtual Task RemoveClaimAsync(TUser user, Claim claim)
///
public virtual async Task RemoveClaimsAsync(TUser user, IEnumerable claims)
{
- ThrowIfDisposed();
- var claimStore = GetClaimStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(claims);
+ try
+ {
+ ThrowIfDisposed();
+ var claimStore = GetClaimStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(claims);
- await claimStore.RemoveClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await claimStore.RemoveClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.RemoveClaims).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RemoveClaims, ex);
+ throw;
+ }
}
///
@@ -1098,6 +1268,21 @@ public virtual async Task> GetClaimsAsync(TUser user)
/// of the operation.
///
public virtual async Task AddToRoleAsync(TUser user, string role)
+ {
+ try
+ {
+ var result = await AddToRoleCoreAsync(user, role).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.AddToRoles);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AddToRoles, ex);
+ throw;
+ }
+ }
+
+ private async Task AddToRoleCoreAsync(TUser user, string role)
{
ThrowIfDisposed();
var userRoleStore = GetUserRoleStore();
@@ -1122,6 +1307,21 @@ public virtual async Task AddToRoleAsync(TUser user, string role
/// of the operation.
///
public virtual async Task AddToRolesAsync(TUser user, IEnumerable roles)
+ {
+ try
+ {
+ var result = await AddToRolesCoreAsync(user, roles).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.AddToRoles);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AddToRoles, ex);
+ throw;
+ }
+ }
+
+ private async Task AddToRolesCoreAsync(TUser user, IEnumerable roles)
{
ThrowIfDisposed();
var userRoleStore = GetUserRoleStore();
@@ -1158,10 +1358,13 @@ public virtual async Task RemoveFromRoleAsync(TUser user, string
var normalizedRole = NormalizeName(role);
if (!await userRoleStore.IsInRoleAsync(user, normalizedRole, CancellationToken).ConfigureAwait(false))
{
- return UserNotInRoleError(role);
+ var failureResult = UserNotInRoleError(role);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, failureResult, UserUpdateType.RemoveFromRoles);
+
+ return failureResult;
}
await userRoleStore.RemoveFromRoleAsync(user, normalizedRole, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.RemoveFromRoles).ConfigureAwait(false);
}
private IdentityResult UserAlreadyInRoleError(string role)
@@ -1192,6 +1395,21 @@ private IdentityResult UserNotInRoleError(string role)
/// of the operation.
///
public virtual async Task RemoveFromRolesAsync(TUser user, IEnumerable roles)
+ {
+ try
+ {
+ var result = await ResolveFromtRolesCoreAsync(user, roles).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.RemoveFromRoles);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RemoveFromRoles, ex);
+ throw;
+ }
+ }
+
+ private async Task ResolveFromtRolesCoreAsync(TUser user, IEnumerable roles)
{
ThrowIfDisposed();
var userRoleStore = GetUserRoleStore();
@@ -1264,14 +1482,22 @@ public virtual async Task IsInRoleAsync(TUser user, string role)
///
public virtual async Task SetEmailAsync(TUser user, string? email)
{
- ThrowIfDisposed();
- var store = GetEmailStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetEmailStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await store.SetEmailAsync(user, email, CancellationToken).ConfigureAwait(false);
- await store.SetEmailConfirmedAsync(user, false, CancellationToken).ConfigureAwait(false);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await store.SetEmailAsync(user, email, CancellationToken).ConfigureAwait(false);
+ await store.SetEmailConfirmedAsync(user, false, CancellationToken).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SetEmail).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetEmail, ex);
+ throw;
+ }
}
///
@@ -1295,8 +1521,8 @@ public virtual async Task SetEmailAsync(TUser user, string? emai
// Need to potentially check all keys
if (user == null && Options.Stores.ProtectPersonalData)
{
- var keyRing = _services.GetService();
- var protector = _services.GetService();
+ var keyRing = ServiceProvider.GetService();
+ var protector = ServiceProvider.GetService();
if (keyRing != null && protector != null)
{
foreach (var key in keyRing.GetAllKeyIds())
@@ -1351,6 +1577,21 @@ public virtual Task GenerateEmailConfirmationTokenAsync(TUser user)
/// of the operation.
///
public virtual async Task ConfirmEmailAsync(TUser user, string token)
+ {
+ try
+ {
+ var result = await ConfirmEmailCoreAsync(user, token).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ConfirmEmail);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ConfirmEmail, ex);
+ throw;
+ }
+ }
+
+ private async Task ConfirmEmailCoreAsync(TUser user, string token)
{
ThrowIfDisposed();
var store = GetEmailStore();
@@ -1406,6 +1647,21 @@ public virtual Task GenerateChangeEmailTokenAsync(TUser user, string new
/// of the operation.
///
public virtual async Task ChangeEmailAsync(TUser user, string newEmail, string token)
+ {
+ try
+ {
+ var result = await ChangeEmailCoreAsync(user, newEmail, token).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ChangeEmail);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ChangeEmail, ex);
+ throw;
+ }
+ }
+
+ private async Task ChangeEmailCoreAsync(TUser user, string newEmail, string token)
{
ThrowIfDisposed();
ArgumentNullThrowHelper.ThrowIfNull(user);
@@ -1419,7 +1675,7 @@ public virtual async Task ChangeEmailAsync(TUser user, string ne
await store.SetEmailAsync(user, newEmail, CancellationToken).ConfigureAwait(false);
await store.SetEmailConfirmedAsync(user, true, CancellationToken).ConfigureAwait(false);
await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.ChangeEmail).ConfigureAwait(false);
}
///
@@ -1446,14 +1702,22 @@ public virtual async Task ChangeEmailAsync(TUser user, string ne
///
public virtual async Task SetPhoneNumberAsync(TUser user, string? phoneNumber)
{
- ThrowIfDisposed();
- var store = GetPhoneNumberStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetPhoneNumberStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await store.SetPhoneNumberAsync(user, phoneNumber, CancellationToken).ConfigureAwait(false);
- await store.SetPhoneNumberConfirmedAsync(user, false, CancellationToken).ConfigureAwait(false);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await store.SetPhoneNumberAsync(user, phoneNumber, CancellationToken).ConfigureAwait(false);
+ await store.SetPhoneNumberConfirmedAsync(user, false, CancellationToken).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SetPhoneNumber).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetPhoneNumber, ex);
+ throw;
+ }
}
///
@@ -1468,6 +1732,21 @@ public virtual async Task SetPhoneNumberAsync(TUser user, string
/// of the operation.
///
public virtual async Task ChangePhoneNumberAsync(TUser user, string phoneNumber, string token)
+ {
+ try
+ {
+ var result = await ChangePhoneNumberCoreAsync(user, phoneNumber, token).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ChangePhoneNumber);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ChangePhoneNumber, ex);
+ throw;
+ }
+ }
+
+ private async Task ChangePhoneNumberCoreAsync(TUser user, string phoneNumber, string token)
{
ThrowIfDisposed();
var store = GetPhoneNumberStore();
@@ -1510,7 +1789,6 @@ public virtual Task IsPhoneNumberConfirmedAsync(TUser user)
///
public virtual Task GenerateChangePhoneNumberTokenAsync(TUser user, string phoneNumber)
{
- ThrowIfDisposed();
return GenerateUserTokenAsync(user, Options.Tokens.ChangePhoneNumberTokenProvider, ChangePhoneNumberTokenPurpose + ":" + phoneNumber);
}
@@ -1548,22 +1826,31 @@ public virtual Task VerifyChangePhoneNumberTokenAsync(TUser user, string t
///
public virtual async Task VerifyUserTokenAsync(TUser user, string tokenProvider, string purpose, string token)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(tokenProvider);
-
- if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ try
{
- throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
- }
- // Make sure the token is valid
- var result = await provider.ValidateAsync(purpose, token, this, user).ConfigureAwait(false);
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(tokenProvider);
- if (!result && Logger.IsEnabled(LogLevel.Debug))
+ if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ {
+ throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
+ }
+ // Make sure the token is valid
+ var result = await provider.ValidateAsync(purpose, token, this, user).ConfigureAwait(false);
+ _metrics?.VerifyToken(typeof(TUser).FullName!, result, purpose);
+
+ if (!result && Logger.IsEnabled(LogLevel.Debug))
+ {
+ Logger.LogDebug(LoggerEventIds.VerifyUserTokenFailed, "VerifyUserTokenAsync() failed with purpose: {purpose} for user.", purpose);
+ }
+ return result;
+ }
+ catch (Exception ex)
{
- Logger.LogDebug(LoggerEventIds.VerifyUserTokenFailed, "VerifyUserTokenAsync() failed with purpose: {purpose} for user.", purpose);
+ _metrics?.VerifyToken(typeof(TUser).FullName!, result: null, purpose, ex);
+ throw;
}
- return result;
}
///
@@ -1578,16 +1865,25 @@ public virtual async Task VerifyUserTokenAsync(TUser user, string tokenPro
///
public virtual Task GenerateUserTokenAsync(TUser user, string tokenProvider, string purpose)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(tokenProvider);
+ try
+ {
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(tokenProvider);
- if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ {
+ throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
+ }
+
+ _metrics?.GenerateToken(typeof(TUser).FullName!, purpose);
+ return provider.GenerateAsync(purpose, this, user);
+ }
+ catch (Exception ex)
{
- throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
+ _metrics?.GenerateToken(typeof(TUser).FullName!, purpose, ex);
+ throw;
}
-
- return provider.GenerateAsync(purpose, this, user);
}
///
@@ -1638,20 +1934,30 @@ public virtual async Task> GetValidTwoFactorProvidersAsync(TUser u
///
public virtual async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ try
{
- throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
- }
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ {
+ throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
+ }
+
+ // Make sure the token is valid
+ var result = await provider.ValidateAsync("TwoFactor", token, this, user).ConfigureAwait(false);
+ _metrics?.VerifyToken(typeof(TUser).FullName!, result, "TwoFactor");
- // Make sure the token is valid
- var result = await provider.ValidateAsync("TwoFactor", token, this, user).ConfigureAwait(false);
- if (!result)
+ if (!result)
+ {
+ Logger.LogDebug(LoggerEventIds.VerifyTwoFactorTokenFailed, $"{nameof(VerifyTwoFactorTokenAsync)}() failed for user.");
+ }
+ return result;
+ }
+ catch (Exception ex)
{
- Logger.LogDebug(LoggerEventIds.VerifyTwoFactorTokenFailed, $"{nameof(VerifyTwoFactorTokenAsync)}() failed for user.");
+ _metrics?.VerifyToken(typeof(TUser).FullName!, result: null, "TwoFactor", ex);
+ throw;
}
- return result;
}
///
@@ -1665,14 +1971,23 @@ public virtual async Task VerifyTwoFactorTokenAsync(TUser user, string tok
///
public virtual Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider)
{
- ThrowIfDisposed();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ try
{
- throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
- }
+ ThrowIfDisposed();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ if (!_tokenProviders.TryGetValue(tokenProvider, out var provider))
+ {
+ throw new NotSupportedException(Resources.FormatNoTokenProvider(nameof(TUser), tokenProvider));
+ }
- return provider.GenerateAsync("TwoFactor", this, user);
+ _metrics?.GenerateToken(typeof(TUser).FullName!, "TwoFactor");
+ return provider.GenerateAsync("TwoFactor", this, user);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.GenerateToken(typeof(TUser).FullName!, "TwoFactor", ex);
+ throw;
+ }
}
///
@@ -1703,13 +2018,21 @@ public virtual async Task GetTwoFactorEnabledAsync(TUser user)
///
public virtual async Task SetTwoFactorEnabledAsync(TUser user, bool enabled)
{
- ThrowIfDisposed();
- var store = GetUserTwoFactorStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetUserTwoFactorStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await store.SetTwoFactorEnabledAsync(user, enabled, CancellationToken).ConfigureAwait(false);
- await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await store.SetTwoFactorEnabledAsync(user, enabled, CancellationToken).ConfigureAwait(false);
+ await UpdateSecurityStampInternal(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SetTwoFactorEnabled).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetTwoFactorEnabled, ex);
+ throw;
+ }
}
///
@@ -1745,12 +2068,20 @@ public virtual async Task IsLockedOutAsync(TUser user)
///
public virtual async Task SetLockoutEnabledAsync(TUser user, bool enabled)
{
- ThrowIfDisposed();
- var store = GetUserLockoutStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetUserLockoutStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- await store.SetLockoutEnabledAsync(user, enabled, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await store.SetLockoutEnabledAsync(user, enabled, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SetLockoutEnabled).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetLockoutEnabled, ex);
+ throw;
+ }
}
///
@@ -1791,6 +2122,21 @@ public virtual async Task GetLockoutEnabledAsync(TUser user)
/// The after which the 's lockout should end.
/// The that represents the asynchronous operation, containing the of the operation.
public virtual async Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd)
+ {
+ try
+ {
+ var result = await SetLockoutEndDateCoreAsync(user, lockoutEnd).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.SetLockoutEndDate);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetLockoutEndDate, ex);
+ throw;
+ }
+ }
+
+ private async Task SetLockoutEndDateCoreAsync(TUser user, DateTimeOffset? lockoutEnd)
{
ThrowIfDisposed();
var store = GetUserLockoutStore();
@@ -1814,21 +2160,29 @@ public virtual async Task SetLockoutEndDateAsync(TUser user, Dat
/// The that represents the asynchronous operation, containing the of the operation.
public virtual async Task AccessFailedAsync(TUser user)
{
- ThrowIfDisposed();
- var store = GetUserLockoutStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetUserLockoutStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
- // If this puts the user over the threshold for lockout, lock them out and reset the access failed count
- var count = await store.IncrementAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
- if (count < Options.Lockout.MaxFailedAccessAttempts)
+ // If this puts the user over the threshold for lockout, lock them out and reset the access failed count
+ var count = await store.IncrementAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
+ if (count < Options.Lockout.MaxFailedAccessAttempts)
+ {
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.AccessFailed).ConfigureAwait(false);
+ }
+ Logger.LogDebug(LoggerEventIds.UserLockedOut, "User is locked out.");
+ await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan),
+ CancellationToken).ConfigureAwait(false);
+ await store.ResetAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.AccessFailed).ConfigureAwait(false);
+ }
+ catch (Exception ex)
{
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.AccessFailed, ex);
+ throw;
}
- Logger.LogDebug(LoggerEventIds.UserLockedOut, "User is locked out.");
- await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan),
- CancellationToken).ConfigureAwait(false);
- await store.ResetAccessFailedCountAsync(user, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
}
///
@@ -1837,6 +2191,21 @@ await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Locko
/// The user whose failed access count should be reset.
/// The that represents the asynchronous operation, containing the of the operation.
public virtual async Task ResetAccessFailedCountAsync(TUser user)
+ {
+ try
+ {
+ var result = await ResetAccessFailedCountCoreAsync(user).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.ResetAccessFailedCount);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.ResetAccessFailedCount, ex);
+ throw;
+ }
+ }
+
+ private async Task ResetAccessFailedCountCoreAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserLockoutStore();
@@ -1925,15 +2294,23 @@ public virtual Task> GetUsersInRoleAsync(string roleName)
/// Whether the user was successfully updated.
public virtual async Task SetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName, string? tokenValue)
{
- ThrowIfDisposed();
- var store = GetAuthenticationTokenStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
- ArgumentNullThrowHelper.ThrowIfNull(tokenName);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetAuthenticationTokenStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
+ ArgumentNullThrowHelper.ThrowIfNull(tokenName);
- // REVIEW: should updating any tokens affect the security stamp?
- await store.SetTokenAsync(user, loginProvider, tokenName, tokenValue, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ // REVIEW: should updating any tokens affect the security stamp?
+ await store.SetTokenAsync(user, loginProvider, tokenName, tokenValue, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.SetAuthenticationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.SetAuthenticationToken, ex);
+ throw;
+ }
}
///
@@ -1945,14 +2322,22 @@ public virtual async Task SetAuthenticationTokenAsync(TUser user
/// Whether a token was removed.
public virtual async Task RemoveAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
{
- ThrowIfDisposed();
- var store = GetAuthenticationTokenStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
- ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
- ArgumentNullThrowHelper.ThrowIfNull(tokenName);
+ try
+ {
+ ThrowIfDisposed();
+ var store = GetAuthenticationTokenStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+ ArgumentNullThrowHelper.ThrowIfNull(loginProvider);
+ ArgumentNullThrowHelper.ThrowIfNull(tokenName);
- await store.RemoveTokenAsync(user, loginProvider, tokenName, CancellationToken).ConfigureAwait(false);
- return await UpdateUserAsync(user).ConfigureAwait(false);
+ await store.RemoveTokenAsync(user, loginProvider, tokenName, CancellationToken).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.RemoveAuthenticationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RemoveAuthenticationToken, ex);
+ throw;
+ }
}
///
@@ -1980,7 +2365,7 @@ public virtual async Task ResetAuthenticatorKeyAsync(TUser user)
ArgumentNullThrowHelper.ThrowIfNull(user);
await store.SetAuthenticatorKeyAsync(user, GenerateNewAuthenticatorKey(), CancellationToken).ConfigureAwait(false);
await UpdateSecurityStampInternal(user).ConfigureAwait(false);
- return await UpdateAsync(user).ConfigureAwait(false);
+ return await UpdateUserAndRecordMetricAsync(user, UserUpdateType.ResetAuthenticatorKey).ConfigureAwait(false);
}
///
@@ -1998,23 +2383,31 @@ public virtual string GenerateNewAuthenticatorKey()
/// The new recovery codes for the user. Note: there may be less than number returned, as duplicates will be removed.
public virtual async Task?> GenerateNewTwoFactorRecoveryCodesAsync(TUser user, int number)
{
- ThrowIfDisposed();
- var store = GetRecoveryCodeStore();
- ArgumentNullThrowHelper.ThrowIfNull(user);
-
- var newCodes = new List(number);
- for (var i = 0; i < number; i++)
+ try
{
- newCodes.Add(CreateTwoFactorRecoveryCode());
- }
+ ThrowIfDisposed();
+ var store = GetRecoveryCodeStore();
+ ArgumentNullThrowHelper.ThrowIfNull(user);
+
+ var newCodes = new List(number);
+ for (var i = 0; i < number; i++)
+ {
+ newCodes.Add(CreateTwoFactorRecoveryCode());
+ }
- await store.ReplaceCodesAsync(user, newCodes.Distinct(), CancellationToken).ConfigureAwait(false);
- var update = await UpdateAsync(user).ConfigureAwait(false);
- if (update.Succeeded)
+ await store.ReplaceCodesAsync(user, newCodes.Distinct(), CancellationToken).ConfigureAwait(false);
+ var update = await UpdateUserAndRecordMetricAsync(user, UserUpdateType.GenerateNewTwoFactorRecoveryCodes).ConfigureAwait(false);
+ if (update.Succeeded)
+ {
+ return newCodes;
+ }
+ return null;
+ }
+ catch (Exception ex)
{
- return newCodes;
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.GenerateNewTwoFactorRecoveryCodes, ex);
+ throw;
}
- return null;
}
///
@@ -2101,6 +2494,21 @@ private static char GetRandomRecoveryCodeChar()
/// The recovery code to use.
/// True if the recovery code was found for the user.
public virtual async Task RedeemTwoFactorRecoveryCodeAsync(TUser user, string code)
+ {
+ try
+ {
+ var result = await RedeemTwoFactorRecoveryCodeCoreAsync(user, code).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, UserUpdateType.RedeemTwoFactorRecoveryCode);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result: null, UserUpdateType.RedeemTwoFactorRecoveryCode, ex);
+ throw;
+ }
+ }
+
+ private async Task RedeemTwoFactorRecoveryCodeCoreAsync(TUser user, string code)
{
ThrowIfDisposed();
var store = GetRecoveryCodeStore();
@@ -2109,7 +2517,7 @@ public virtual async Task RedeemTwoFactorRecoveryCodeAsync(TUser
var success = await store.RedeemCodeAsync(user, code, CancellationToken).ConfigureAwait(false);
if (success)
{
- return await UpdateAsync(user).ConfigureAwait(false);
+ return await UpdateUserAsync(user).ConfigureAwait(false);
}
return IdentityResult.Failed(ErrorDescriber.RecoveryCodeRedemptionFailed());
}
@@ -2298,6 +2706,8 @@ private IUserClaimStore GetClaimStore()
/// A representing whether validation was successful.
protected async Task ValidateUserAsync(TUser user)
{
+ // NOTE: Metrics aren't recorded here. Instead, the result is used in other methods.
+
if (SupportsUserSecurityStamp)
{
var stamp = await GetSecurityStampAsync(user).ConfigureAwait(false);
@@ -2370,6 +2780,8 @@ protected async Task ValidatePasswordAsync(TUser user, string? p
/// Whether the operation was successful.
protected virtual async Task UpdateUserAsync(TUser user)
{
+ // NOTE: Metrics aren't recorded here. Instead, the result is used in other methods.
+
var result = await ValidateUserAsync(user).ConfigureAwait(false);
if (!result.Succeeded)
{
@@ -2380,6 +2792,14 @@ protected virtual async Task UpdateUserAsync(TUser user)
return await Store.UpdateAsync(user, CancellationToken).ConfigureAwait(false);
}
+ private async Task UpdateUserAndRecordMetricAsync(TUser user, UserUpdateType updateType)
+ {
+ var result = await UpdateUserAsync(user).ConfigureAwait(false);
+ _metrics?.UpdateUser(typeof(TUser).FullName!, result, updateType);
+
+ return result;
+ }
+
private IUserAuthenticatorKeyStore GetAuthenticatorKeyStore()
{
var cast = Store as IUserAuthenticatorKeyStore;
diff --git a/src/Identity/Extensions.Core/src/UserManagerMetrics.cs b/src/Identity/Extensions.Core/src/UserManagerMetrics.cs
new file mode 100644
index 000000000000..6e25c86db986
--- /dev/null
+++ b/src/Identity/Extensions.Core/src/UserManagerMetrics.cs
@@ -0,0 +1,284 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Linq;
+using System.Threading.Tasks;
+using static Microsoft.AspNetCore.Identity.UserManagerMetrics;
+
+namespace Microsoft.AspNetCore.Identity;
+
+internal sealed class UserManagerMetrics : IDisposable
+{
+ public const string MeterName = "Microsoft.AspNetCore.Identity";
+
+ public const string CreateCounterName = "aspnetcore.identity.user.create";
+ public const string UpdateCounterName = "aspnetcore.identity.user.update";
+ public const string DeleteCounterName = "aspnetcore.identity.user.delete";
+ public const string CheckPasswordCounterName = "aspnetcore.identity.user.check_password";
+ public const string VerifyPasswordCounterName = "aspnetcore.identity.user.verify_password";
+ public const string VerifyTokenCounterName = "aspnetcore.identity.user.verify_token";
+ public const string GenerateTokenCounterName = "aspnetcore.identity.user.generate_token";
+
+ private readonly Meter _meter;
+ private readonly Counter _createCounter;
+ private readonly Counter _updateCounter;
+ private readonly Counter _deleteCounter;
+ private readonly Counter _checkPasswordCounter;
+ private readonly Counter _verifyPasswordCounter;
+ private readonly Counter _verifyTokenCounter;
+ private readonly Counter _generateTokenCounter;
+
+ public UserManagerMetrics(IMeterFactory meterFactory)
+ {
+ _meter = meterFactory.Create(MeterName);
+ _createCounter = _meter.CreateCounter(CreateCounterName, "count", "The number of users created.");
+ _updateCounter = _meter.CreateCounter(UpdateCounterName, "count", "The number of user updates.");
+ _deleteCounter = _meter.CreateCounter(DeleteCounterName, "count", "The number of users deleted.");
+ _checkPasswordCounter = _meter.CreateCounter(CheckPasswordCounterName, "count", "The number of check password attempts.");
+ _verifyPasswordCounter = _meter.CreateCounter(VerifyPasswordCounterName, "count", "The number of password verification attempts.");
+ _verifyTokenCounter = _meter.CreateCounter(VerifyTokenCounterName, "count", "The number of token verification attempts.");
+ _generateTokenCounter = _meter.CreateCounter(GenerateTokenCounterName, "count", "The number of token generation attempts.");
+ }
+
+ internal void CreateUser(string userType, IdentityResult? result, Exception? exception = null)
+ {
+ if (_createCounter.Enabled)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType }
+ };
+ AddIdentityResultTags(ref tags, result);
+ AddExceptionTags(ref tags, exception);
+
+ _createCounter.Add(1, tags);
+ }
+ }
+
+ private static void AddExceptionTags(ref TagList tags, Exception? exception)
+ {
+ if (exception != null)
+ {
+ tags.Add("error.type", exception.GetType().FullName!);
+ }
+ }
+
+ internal void UpdateUser(string userType, IdentityResult? result, UserUpdateType updateType, Exception? exception = null)
+ {
+ if (_updateCounter.Enabled)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.user.update_type", GetUpdateType(updateType) },
+ };
+ AddIdentityResultTags(ref tags, result);
+ AddExceptionTags(ref tags, exception);
+
+ _updateCounter.Add(1, tags);
+ }
+ }
+
+ internal void DeleteUser(string userType, IdentityResult? result, Exception? exception = null)
+ {
+ if (_deleteCounter.Enabled)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType }
+ };
+ AddIdentityResultTags(ref tags, result);
+ AddExceptionTags(ref tags, exception);
+
+ _deleteCounter.Add(1, tags);
+ }
+ }
+
+ internal void CheckPassword(string userType, bool? userMissing, PasswordVerificationResult? result, Exception? exception = null)
+ {
+ if (_checkPasswordCounter.Enabled)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ };
+ if (userMissing != null || result != null)
+ {
+ tags.Add("aspnetcore.identity.user.password_result", GetPasswordResult(result, passwordMissing: null, userMissing));
+ }
+ AddExceptionTags(ref tags, exception);
+
+ _checkPasswordCounter.Add(1, tags);
+ }
+ }
+
+ internal void VerifyPassword(string userType, bool passwordMissing, PasswordVerificationResult? result)
+ {
+ if (_verifyPasswordCounter.Enabled)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.user.password_result", GetPasswordResult(result, passwordMissing, userMissing: null) },
+ };
+
+ _verifyPasswordCounter.Add(1, tags);
+ }
+ }
+
+ internal void VerifyToken(string userType, bool? result, string purpose, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.token_purpose", GetTokenPurpose(purpose) },
+ };
+ if (result != null)
+ {
+ tags.Add("aspnetcore.identity.token_verified", result == true ? "success" : "failure");
+ }
+ AddExceptionTags(ref tags, exception);
+
+ _verifyTokenCounter.Add(1, tags);
+ }
+
+ internal void GenerateToken(string userType, string purpose, Exception? exception = null)
+ {
+ var tags = new TagList
+ {
+ { "aspnetcore.identity.user_type", userType },
+ { "aspnetcore.identity.token_purpose", GetTokenPurpose(purpose) },
+ };
+ AddExceptionTags(ref tags, exception);
+
+ _generateTokenCounter.Add(1, tags);
+ }
+
+ private static string GetTokenPurpose(string purpose)
+ {
+ // Purpose could be any value and can't be used as a tag value. However, there are known purposes
+ // on UserManager that we can detect and use as a tag value. Some could have a ':' in them followed by user data.
+ // We need to trim them to content before ':' and then match to known values.
+ var trimmedPurpose = purpose;
+ var colonIndex = purpose.IndexOf(':');
+ if (colonIndex >= 0)
+ {
+ trimmedPurpose = purpose.Substring(0, colonIndex);
+ }
+
+ return trimmedPurpose switch
+ {
+ "ResetPassword" => "reset_password",
+ "ChangePhoneNumber" => "change_phone_number",
+ "EmailConfirmation" => "email_confirmation",
+ "ChangeEmail" => "change_email",
+ "TwoFactor" => "two_factor",
+ _ => "_UNKNOWN"
+ };
+ }
+
+ private static void AddIdentityResultTags(ref TagList tags, IdentityResult? result)
+ {
+ if (result == null)
+ {
+ return;
+ }
+
+ tags.Add("aspnetcore.identity.result", result.Succeeded ? "success" : "failure");
+ if (!result.Succeeded && result.Errors.FirstOrDefault()?.Code is { Length: > 0 } code)
+ {
+ tags.Add("aspnetcore.identity.result_error_code", code);
+ }
+ }
+
+ private static string GetPasswordResult(PasswordVerificationResult? result, bool? passwordMissing, bool? userMissing)
+ {
+ return (result, passwordMissing ?? false, userMissing ?? false) switch
+ {
+ (PasswordVerificationResult.Success, false, false) => "success",
+ (PasswordVerificationResult.SuccessRehashNeeded, false, false) => "success_rehash_needed",
+ (PasswordVerificationResult.Failed, false, false) => "failure",
+ (null, true, false) => "password_missing",
+ (null, false, true) => "user_missing",
+ _ => "_UNKNOWN"
+ };
+ }
+
+ private static string GetUpdateType(UserUpdateType updateType)
+ {
+ return updateType switch
+ {
+ UserUpdateType.Update => "update",
+ UserUpdateType.UserName => "user_name",
+ UserUpdateType.AddPassword => "add_password",
+ UserUpdateType.ChangePassword => "change_password",
+ UserUpdateType.SecurityStamp => "security_stamp",
+ UserUpdateType.ResetPassword => "reset_password",
+ UserUpdateType.RemoveLogin => "remove_login",
+ UserUpdateType.AddLogin => "add_login",
+ UserUpdateType.AddClaims => "add_claims",
+ UserUpdateType.ReplaceClaim => "replace_claim",
+ UserUpdateType.RemoveClaims => "remove_claims",
+ UserUpdateType.AddToRoles => "add_to_roles",
+ UserUpdateType.RemoveFromRoles => "remove_from_roles",
+ UserUpdateType.SetEmail => "set_email",
+ UserUpdateType.ConfirmEmail => "confirm_email",
+ UserUpdateType.PasswordRehash => "password_rehash",
+ UserUpdateType.RemovePassword => "remove_password",
+ UserUpdateType.ChangeEmail => "change_email",
+ UserUpdateType.SetPhoneNumber => "set_phone_number",
+ UserUpdateType.ChangePhoneNumber => "change_phone_number",
+ UserUpdateType.SetTwoFactorEnabled => "set_two_factor_enabled",
+ UserUpdateType.SetLockoutEnabled => "set_lockout_enabled",
+ UserUpdateType.SetLockoutEndDate => "set_lockout_end_date",
+ UserUpdateType.AccessFailed => "access_failed",
+ UserUpdateType.ResetAccessFailedCount => "reset_access_failed_count",
+ UserUpdateType.SetAuthenticationToken => "set_authentication_token",
+ UserUpdateType.RemoveAuthenticationToken => "remove_authentication_token",
+ UserUpdateType.ResetAuthenticatorKey => "reset_authenticator_key",
+ _ => "_UNKNOWN"
+ };
+ }
+
+ public void Dispose()
+ {
+ _meter.Dispose();
+ }
+}
+
+internal enum UserUpdateType
+{
+ Update,
+ UserName,
+ AddPassword,
+ ChangePassword,
+ SecurityStamp,
+ ResetPassword,
+ RemoveLogin,
+ AddLogin,
+ AddClaims,
+ ReplaceClaim,
+ RemoveClaims,
+ AddToRoles,
+ RemoveFromRoles,
+ SetEmail,
+ ConfirmEmail,
+ PasswordRehash,
+ RemovePassword,
+ ChangeEmail,
+ SetPhoneNumber,
+ ChangePhoneNumber,
+ SetTwoFactorEnabled,
+ SetLockoutEnabled,
+ SetLockoutEndDate,
+ AccessFailed,
+ ResetAccessFailedCount,
+ SetAuthenticationToken,
+ RemoveAuthenticationToken,
+ ResetAuthenticatorKey,
+ GenerateNewTwoFactorRecoveryCodes,
+ RedeemTwoFactorRecoveryCode,
+}
diff --git a/src/Identity/Identity.slnf b/src/Identity/Identity.slnf
index 8e6b1617cb3d..1a8aba8f8489 100644
--- a/src/Identity/Identity.slnf
+++ b/src/Identity/Identity.slnf
@@ -10,6 +10,7 @@
"src\\DataProtection\\Cryptography.KeyDerivation\\src\\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj",
"src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
"src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj",
+ "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
@@ -50,6 +51,7 @@
"src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
"src\\Middleware\\Diagnostics.EntityFrameworkCore\\src\\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj",
"src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
+ "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj",
"src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
"src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj",
"src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj",
@@ -74,19 +76,25 @@
"src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
"src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
"src\\Security\\Authentication\\BearerToken\\src\\Microsoft.AspNetCore.Authentication.BearerToken.csproj",
+ "src\\Security\\Authentication\\Certificate\\src\\Microsoft.AspNetCore.Authentication.Certificate.csproj",
"src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
"src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
"src\\Security\\Authentication\\Facebook\\src\\Microsoft.AspNetCore.Authentication.Facebook.csproj",
"src\\Security\\Authentication\\Google\\src\\Microsoft.AspNetCore.Authentication.Google.csproj",
"src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj",
+ "src\\Security\\Authentication\\MicrosoftAccount\\src\\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj",
"src\\Security\\Authentication\\OAuth\\src\\Microsoft.AspNetCore.Authentication.OAuth.csproj",
+ "src\\Security\\Authentication\\OpenIdConnect\\src\\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj",
"src\\Security\\Authentication\\Twitter\\src\\Microsoft.AspNetCore.Authentication.Twitter.csproj",
+ "src\\Security\\Authentication\\WsFederation\\src\\Microsoft.AspNetCore.Authentication.WsFederation.csproj",
"src\\Security\\Authentication\\test\\Microsoft.AspNetCore.Authentication.Test.csproj",
"src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
"src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
"src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj",
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
+ "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
"src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
+ "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj",
"src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
"src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
"src\\Servers\\Kestrel\\Transport.NamedPipes\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj",
@@ -96,4 +104,4 @@
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj b/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj
index 798e4dc3db40..456637020d55 100644
--- a/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj
+++ b/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj
@@ -1,4 +1,4 @@
-
+
$(DefaultNetCoreTargetFramework)
@@ -6,6 +6,7 @@
+
@@ -16,6 +17,7 @@
+
diff --git a/src/Identity/test/Identity.Test/SignInManagerTest.cs b/src/Identity/test/Identity.Test/SignInManagerTest.cs
index 73fe6d6be218..866ad8640da3 100644
--- a/src/Identity/test/Identity.Test/SignInManagerTest.cs
+++ b/src/Identity/test/Identity.Test/SignInManagerTest.cs
@@ -1,11 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@@ -31,8 +35,12 @@ public void ConstructorNullChecks()
public async Task PasswordSignInReturnsLockedOutWhenLockedOut()
{
// Setup
+ var testMeterFactory = new TestMeterFactory();
+ using var authenticate = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateCounterName);
+ using var signInUserPrincipal = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInUserPrincipalCounterName);
+
var user = new PocoUser { UserName = "Foo" };
- var manager = SetupUserManager(user);
+ var manager = SetupUserManager(user, meterFactory: testMeterFactory);
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable();
@@ -55,14 +63,28 @@ public async Task PasswordSignInReturnsLockedOutWhenLockedOut()
Assert.True(result.IsLockedOut);
Assert.Contains($"User is currently locked out.", logger.LogMessages);
manager.Verify();
+
+ Assert.Collection(authenticate.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
+ KeyValuePair.Create("aspnetcore.identity.authentication_scheme", "Identity.Application"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.type", "password"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.is_persistent", false),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.result", "locked_out"),
+ ]));
+ Assert.Empty(signInUserPrincipal.GetMeasurementSnapshot());
}
[Fact]
public async Task CheckPasswordSignInReturnsLockedOutWhenLockedOut()
{
// Setup
+ var testMeterFactory = new TestMeterFactory();
+ using var checkPasswordSignIn = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.CheckPasswordCounterName);
+
var user = new PocoUser { UserName = "Foo" };
- var manager = SetupUserManager(user);
+ var manager = SetupUserManager(user, meterFactory: testMeterFactory);
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable();
@@ -85,11 +107,18 @@ public async Task CheckPasswordSignInReturnsLockedOutWhenLockedOut()
Assert.True(result.IsLockedOut);
Assert.Contains($"User is currently locked out.", logger.LogMessages);
manager.Verify();
+
+ Assert.Collection(checkPasswordSignIn.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.result", "locked_out"),
+ ]));
}
- private static Mock> SetupUserManager(PocoUser user)
+ private static Mock> SetupUserManager(PocoUser user, IMeterFactory meterFactory = null)
{
- var manager = MockHelpers.MockUserManager();
+ var manager = MockHelpers.MockUserManager(meterFactory);
manager.Setup(m => m.FindByNameAsync(user.UserName)).ReturnsAsync(user);
manager.Setup(m => m.FindByIdAsync(user.Id)).ReturnsAsync(user);
manager.Setup(m => m.GetUserIdAsync(user)).ReturnsAsync(user.Id.ToString());
@@ -300,10 +329,14 @@ public async Task PasswordSignInRequiresVerification(bool supportsLockout)
public async Task ExternalSignInRequiresVerificationIfNotBypassed(bool bypass)
{
// Setup
+ var testMeterFactory = new TestMeterFactory();
+ using var authenticate = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.AuthenticateCounterName);
+ using var signInUserPrincipal = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInUserPrincipalCounterName);
+
var user = new PocoUser { UserName = "Foo" };
const string loginProvider = "login";
const string providerKey = "fookey";
- var manager = SetupUserManager(user);
+ var manager = SetupUserManager(user, meterFactory: testMeterFactory);
manager.Setup(m => m.SupportsUserLockout).Returns(false).Verifiable();
manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable();
if (!bypass)
@@ -337,6 +370,38 @@ public async Task ExternalSignInRequiresVerificationIfNotBypassed(bool bypass)
Assert.Equal(!bypass, result.RequiresTwoFactor);
manager.Verify();
auth.Verify();
+
+ if (bypass)
+ {
+ Assert.Collection(authenticate.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
+ KeyValuePair.Create("aspnetcore.identity.authentication_scheme", "Identity.Application"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.type", "external"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.is_persistent", false),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.result", "success"),
+ ]));
+ Assert.Collection(signInUserPrincipal.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
+ KeyValuePair.Create("aspnetcore.identity.authentication_scheme", "Identity.Application"),
+ ]));
+ }
+ else
+ {
+ Assert.Collection(authenticate.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create("aspnetcore.identity.user_type", "Microsoft.AspNetCore.Identity.Test.PocoUser"),
+ KeyValuePair.Create("aspnetcore.identity.authentication_scheme", "Identity.Application"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.type", "external"),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.is_persistent", false),
+ KeyValuePair.Create("aspnetcore.identity.sign_in.result", "requires_two_factor"),
+ ]));
+ Assert.Empty(signInUserPrincipal.GetMeasurementSnapshot());
+ }
}
private class GoodTokenProvider : AuthenticatorTokenProvider
@@ -587,6 +652,38 @@ public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout)
auth.Verify();
}
+ [Fact]
+ public async Task SignInAsync_Failure()
+ {
+ // Setup
+ var testMeterFactory = new TestMeterFactory();
+ using var signInUserPrincipal = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Identity", SignInManagerMetrics.SignInUserPrincipalCounterName);
+
+ var user = new PocoUser { UserName = "Foo" };
+ var manager = SetupUserManager(user, meterFactory: testMeterFactory);
+
+ var context = new DefaultHttpContext();
+ var auth = MockAuth(context);
+ var helper = SetupSignInManager(manager.Object, context);
+ auth.Setup(a => a.SignInAsync(context, IdentityConstants.ApplicationScheme, It.IsAny(), It.IsAny()))
+ .Throws(new InvalidOperationException("SignInAsync failed")).Verifiable();
+
+ // Act
+ await Assert.ThrowsAsync(() => helper.SignInAsync(user, isPersistent: false));
+
+ // Assert
+ manager.Verify();
+ auth.Verify();
+
+ Assert.Collection(signInUserPrincipal.GetMeasurementSnapshot(),
+ m => MetricsHelpers.AssertContainsTags(m.Tags,
+ [
+ KeyValuePair.Create