From 971f727ea28a2b244ed1684e07b913009df537ef Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Tue, 27 Mar 2018 15:52:06 -0700 Subject: [PATCH] [Fixes #1690] AddDefaultUI() throws when using IdentityUser --- Identity.sln | 2 +- .../Pages/Account/ConfirmEmail.cshtml.cs | 2 +- .../Pages/Account/ExternalLogin.cshtml.cs | 7 +++-- .../Pages/Account/ForgotPassword.cshtml.cs | 2 +- .../Identity/Pages/Account/Login.cshtml.cs | 2 +- .../Pages/Account/LoginWith2fa.cshtml.cs | 17 +++++++---- .../Account/LoginWithRecoveryCode.cshtml.cs | 17 +++++++---- .../Identity/Pages/Account/Logout.cshtml.cs | 2 +- .../Account/Manage/ChangePassword.cshtml.cs | 2 +- .../Manage/DeletePersonalData.cshtml.cs | 7 +++-- .../Pages/Account/Manage/Disable2fa.cshtml.cs | 2 +- .../Manage/DownloadPersonalData.cshtml.cs | 2 +- .../Manage/EnableAuthenticator.cshtml.cs | 9 ++++-- .../Account/Manage/ExternalLogins.cshtml.cs | 27 ++++++++++++----- .../Manage/GenerateRecoveryCodes.cshtml.cs | 16 ++++++---- .../Pages/Account/Manage/Index.cshtml.cs | 30 ++++++++++++------- .../Account/Manage/PersonalData.cshtml.cs | 2 +- .../Manage/ResetAuthenticator.cshtml.cs | 5 ++-- .../Account/Manage/SetPassword.cshtml.cs | 2 +- .../Manage/TwoFactorAuthentication.cshtml.cs | 2 +- .../Identity/Pages/Account/Register.cshtml.cs | 10 +++++-- .../Pages/Account/ResetPassword.cshtml.cs | 2 +- src/UI/IUserFactory.cs | 20 +++++++++++++ src/UI/IdentityBuilderUIExtensions.cs | 27 +++++++++++++++++ src/UI/IdentityDefaultUIConfigureOptions.cs | 2 +- src/UI/IdentityPageModelConvention.cs | 9 ++---- src/UI/UserFactory.cs | 17 +++++++++++ 27 files changed, 178 insertions(+), 66 deletions(-) create mode 100644 src/UI/IUserFactory.cs create mode 100644 src/UI/UserFactory.cs diff --git a/Identity.sln b/Identity.sln index aa69c50755..8fca1061c2 100644 --- a/Identity.sln +++ b/Identity.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27130.2027 MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F647068-6602-4E24-B1DC-8ED91481A50A}" ProjectSection(SolutionItems) = preProject diff --git a/src/UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs index 8ad4f34427..daa126c877 100644 --- a/src/UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnGetAsync(string userId, string code) => throw new NotImplementedException(); } - internal class ConfirmEmailModel : ConfirmEmailModel where TUser : IdentityUser + internal class ConfirmEmailModel : ConfirmEmailModel where TUser : class { private readonly UserManager _userManager; diff --git a/src/UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs index 8a04c751b7..2017cd257c 100644 --- a/src/UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -40,19 +40,22 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostConfirmationAsync(string returnUrl = null) => throw new NotImplementedException(); } - internal class ExternalLoginModel : ExternalLoginModel where TUser : IdentityUser, new() + internal class ExternalLoginModel : ExternalLoginModel where TUser : class { private readonly SignInManager _signInManager; private readonly UserManager _userManager; + private readonly IUserFactory _userFactory; private readonly ILogger _logger; public ExternalLoginModel( SignInManager signInManager, UserManager userManager, + IUserFactory userFactory, ILogger logger) { _signInManager = signInManager; _userManager = userManager; + _userFactory = userFactory; _logger = logger; } @@ -124,7 +127,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal if (ModelState.IsValid) { - var user = new TUser { UserName = Input.Email, Email = Input.Email }; + var user = _userFactory.CreateUser(email: Input.Email, userName: Input.Email); var result = await _userManager.CreateAsync(user); if (result.Succeeded) { diff --git a/src/UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index bdbe417934..b3ba40c5e6 100644 --- a/src/UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class ForgotPasswordModel : ForgotPasswordModel where TUser : IdentityUser + internal class ForgotPasswordModel : ForgotPasswordModel where TUser : class { private readonly UserManager _userManager; private readonly IEmailSender _emailSender; diff --git a/src/UI/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Login.cshtml.cs index 0fac6f2506..733913be01 100644 --- a/src/UI/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync(string returnUrl = null) => throw new NotImplementedException(); } - internal class LoginModel : LoginModel where TUser : IdentityUser + internal class LoginModel : LoginModel where TUser : class { private readonly SignInManager _signInManager; private readonly ILogger _logger; diff --git a/src/UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs index b9ef568f57..3bda67df0b 100644 --- a/src/UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs @@ -39,14 +39,19 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync(bool rememberMe, string returnUrl = null) => throw new NotImplementedException(); } - internal class LoginWith2faModel : LoginWith2faModel where TUser : IdentityUser + internal class LoginWith2faModel : LoginWith2faModel where TUser : class { private readonly SignInManager _signInManager; + private readonly UserManager _userManager; private readonly ILogger _logger; - public LoginWith2faModel(SignInManager signInManager, ILogger logger) + public LoginWith2faModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) { _signInManager = signInManager; + _userManager = userManager; _logger = logger; } @@ -85,19 +90,21 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); + var userId = await _userManager.GetUserIdAsync(user); + if (result.Succeeded) { - _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); + _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId); return LocalRedirect(returnUrl); } else if (result.IsLockedOut) { - _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + _logger.LogWarning("User with ID '{UserId}' account locked out.", userId); return RedirectToPage("./Lockout"); } else { - _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); + _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); ModelState.AddModelError(string.Empty, "Invalid authenticator code."); return Page(); } diff --git a/src/UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs index ea91bec3ab..47a65af546 100644 --- a/src/UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -34,14 +34,19 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync(string returnUrl = null) => throw new NotImplementedException(); } - internal class LoginWithRecoveryCodeModel : LoginWithRecoveryCodeModel where TUser : IdentityUser + internal class LoginWithRecoveryCodeModel : LoginWithRecoveryCodeModel where TUser: class { private readonly SignInManager _signInManager; + private readonly UserManager _userManager; private readonly ILogger _logger; - public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) + public LoginWithRecoveryCodeModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) { _signInManager = signInManager; + _userManager = userManager; _logger = logger; } @@ -76,19 +81,21 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + var userId = await _userManager.GetUserIdAsync(user); + if (result.Succeeded) { - _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); + _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); return LocalRedirect(returnUrl ?? Url.Content("~/")); } if (result.IsLockedOut) { - _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + _logger.LogWarning("User with ID '{UserId}' account locked out.", userId); return RedirectToPage("./Lockout"); } else { - _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); + _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); return Page(); } diff --git a/src/UI/Areas/Identity/Pages/Account/Logout.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Logout.cshtml.cs index adf9edced2..30c2253631 100644 --- a/src/UI/Areas/Identity/Pages/Account/Logout.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPost(string returnUrl = null) => throw new NotImplementedException(); } - internal class LogoutModel : LogoutModel where TUser : IdentityUser + internal class LogoutModel : LogoutModel where TUser : class { private readonly SignInManager _signInManager; private readonly ILogger _logger; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs index 32569fdc0c..8e67feb4cf 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class ChangePasswordModel : ChangePasswordModel where TUser : IdentityUser + internal class ChangePasswordModel : ChangePasswordModel where TUser : class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs index 7d7b074d2b..7d0dbf4c18 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class DeletePersonalDataModel : DeletePersonalDataModel where TUser : IdentityUser + internal class DeletePersonalDataModel : DeletePersonalDataModel where TUser: class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -77,14 +77,15 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal } var result = await _userManager.DeleteAsync(user); + var userId = await _userManager.GetUserIdAsync(user); if (!result.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred deleteing user with ID '{user.Id}'."); + throw new InvalidOperationException($"Unexpected error occurred deleteing user with ID '{userId}'."); } await _signInManager.SignOutAsync(); - _logger.LogInformation("User with ID '{UserId}' deleted themselves.", _userManager.GetUserId(User)); + _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); return Redirect("~/"); } diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs index d0c0f70d4b..be9493db40 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class Disable2faModel : Disable2faModel where TUser : IdentityUser + internal class Disable2faModel : Disable2faModel where TUser : class { private readonly UserManager _userManager; private readonly ILogger _logger; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs index d7bcb2d231..c5feda5dd4 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class DownloadPersonalDataModel : DownloadPersonalDataModel where TUser : IdentityUser + internal class DownloadPersonalDataModel : DownloadPersonalDataModel where TUser : class { private readonly UserManager _userManager; private readonly ILogger _logger; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index db0a93e7fd..5e5bb89445 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class EnableAuthenticatorModel : EnableAuthenticatorModel where TUser : IdentityUser + internal class EnableAuthenticatorModel : EnableAuthenticatorModel where TUser : class { private readonly UserManager _userManager; private readonly ILogger _logger; @@ -102,7 +102,8 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal } await _userManager.SetTwoFactorEnabledAsync(user, true); - _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); StatusMessage = "Your authenticator app has been verified."; @@ -129,7 +130,9 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal } SharedKey = FormatKey(unformattedKey); - AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + + var email = await _userManager.GetEmailAsync(user); + AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey); } private string FormatKey(string unformattedKey) diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs index 256f62335c..ee3df64262 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; @@ -32,17 +33,20 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnGetLinkLoginCallbackAsync() => throw new NotImplementedException(); } - internal class ExternalLoginsModel : ExternalLoginsModel where TUser : IdentityUser + internal class ExternalLoginsModel : ExternalLoginsModel where TUser : class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; + private readonly IUserStore _userStore; public ExternalLoginsModel( UserManager userManager, - SignInManager signInManager) + SignInManager signInManager, + IUserStore userStore) { _userManager = userManager; _signInManager = signInManager; + _userStore = userStore; } public override async Task OnGetAsync() @@ -57,7 +61,14 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) .ToList(); - ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + + string passwordHash = null; + if (_userStore is IUserPasswordStore userPasswordStore) + { + passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); + } + + ShowRemoveButton = passwordHash != null || CurrentLogins.Count > 1; return Page(); } @@ -72,7 +83,8 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); if (!result.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred removing external login for user with ID '{userId}'."); } await _signInManager.RefreshSignInAsync(user); @@ -99,16 +111,17 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + var userId = await _userManager.GetUserIdAsync(user); + var info = await _signInManager.GetExternalLoginInfoAsync(userId); if (info == null) { - throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{userId}'."); } var result = await _userManager.AddLoginAsync(user, info); if (!result.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); + throw new InvalidOperationException($"Unexpected error occurred adding external login for user with ID '{userId}'."); } // Clear the existing external cookie to ensure a clean login process diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs index c1e7250b8a..434948c3b8 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class GenerateRecoveryCodesModel : GenerateRecoveryCodesModel where TUser : IdentityUser + internal class GenerateRecoveryCodesModel : GenerateRecoveryCodesModel where TUser : class { private readonly UserManager _userManager; private readonly ILogger _logger; @@ -45,9 +45,11 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - if (!user.TwoFactorEnabled) + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) { - throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{user.Id}' because they do not have 2FA enabled."); + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled."); } return Page(); @@ -61,15 +63,17 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - if (!user.TwoFactorEnabled) + var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + var userId = await _userManager.GetUserIdAsync(user); + if (!isTwoFactorEnabled) { - throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled."); } var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); RecoveryCodes = recoveryCodes.ToArray(); - _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); StatusMessage = "You have generated new recovery codes."; return RedirectToPage("./ShowRecoveryCodes"); } diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs index 6f84a1e903..0534aa97ee 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostSendVerificationEmailAsync() => throw new NotImplementedException(); } - internal class IndexModel : IndexModel where TUser : IdentityUser + internal class IndexModel : IndexModel where TUser : class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -65,13 +65,16 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal { return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } + var userName = await _userManager.GetUserNameAsync(user); + var email = await _userManager.GetEmailAsync(user); + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); - Username = user.UserName; + Username = userName; Input = new InputModel { - Email = user.Email, - PhoneNumber = user.PhoneNumber + Email = email, + PhoneNumber = phoneNumber }; IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); @@ -92,21 +95,25 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - if (Input.Email != user.Email) + var email = await _userManager.GetEmailAsync(user); + if (Input.Email != email) { var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); if (!setEmailResult.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'."); } } - if (Input.PhoneNumber != user.PhoneNumber) + var phoneNumber = await _userManager.GetPhoneNumberAsync(user); + if (Input.PhoneNumber != phoneNumber) { var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); if (!setPhoneResult.Succeeded) { - throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); + var userId = await _userManager.GetUserIdAsync(user); + throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'."); } } @@ -128,14 +135,17 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } + + var userId = await _userManager.GetUserIdAsync(user); + var email = await _userManager.GetEmailAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, - values: new { userId = user.Id, code = code }, + values: new { userId = userId, code = code }, protocol: Request.Scheme); await _emailSender.SendEmailAsync( - user.Email, + email, "Confirm your email", $"Please confirm your account by clicking here."); diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs index 11e0e8ba4f..ebe50d4aae 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnGet() => throw new NotImplementedException(); } - internal class PersonalDataModel : PersonalDataModel where TUser : IdentityUser + internal class PersonalDataModel : PersonalDataModel where TUser : class { private readonly UserManager _userManager; private readonly ILogger _logger; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs index b0d977b4e6..1df3621166 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class ResetAuthenticatorModel : ResetAuthenticatorModel where TUser : IdentityUser + internal class ResetAuthenticatorModel : ResetAuthenticatorModel where TUser : class { UserManager _userManager; private readonly SignInManager _signInManager; @@ -57,7 +57,8 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal await _userManager.SetTwoFactorEnabledAsync(user, false); await _userManager.ResetAuthenticatorKeyAsync(user); - _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + var userId = await _userManager.GetUserIdAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); await _signInManager.RefreshSignInAsync(user); StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key."; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs index a2092c7be5..9424203802 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class SetPasswordModel : SetPasswordModel where TUser : IdentityUser + internal class SetPasswordModel : SetPasswordModel where TUser : class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs index 08cc7283e4..7c471aac57 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal } - internal class TwoFactorAuthenticationModel : TwoFactorAuthenticationModel where TUser : IdentityUser + internal class TwoFactorAuthenticationModel : TwoFactorAuthenticationModel where TUser : class { private readonly UserManager _userManager; private readonly SignInManager _signInManager; diff --git a/src/UI/Areas/Identity/Pages/Account/Register.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Register.cshtml.cs index ade3184c6a..ded2d74162 100644 --- a/src/UI/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -46,9 +46,10 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync(string returnUrl = null) => throw new NotImplementedException(); } - internal class RegisterModel : RegisterModel where TUser : IdentityUser, new() + internal class RegisterModel : RegisterModel where TUser : class { private readonly SignInManager _signInManager; + private readonly IUserFactory _userFactory; private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IEmailSender _emailSender; @@ -56,11 +57,13 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public RegisterModel( UserManager userManager, SignInManager signInManager, + IUserFactory userFactory, ILogger logger, IEmailSender emailSender) { _userManager = userManager; _signInManager = signInManager; + _userFactory = userFactory; _logger = logger; _emailSender = emailSender; } @@ -75,17 +78,18 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal returnUrl = returnUrl ?? Url.Content("~/"); if (ModelState.IsValid) { - var user = new TUser { UserName = Input.Email, Email = Input.Email }; + var user = _userFactory.CreateUser(email: Input.Email, userName: Input.Email); var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); + var userId = await _userManager.GetUserIdAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, - values: new { userId = user.Id, code = code }, + values: new { userId = userId, code = code }, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", diff --git a/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs index 8e8511aff0..8b2f9c00dc 100644 --- a/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal public virtual Task OnPostAsync() => throw new NotImplementedException(); } - internal class ResetPasswordModel : ResetPasswordModel where TUser : IdentityUser + internal class ResetPasswordModel : ResetPasswordModel where TUser : class { private readonly UserManager _userManager; diff --git a/src/UI/IUserFactory.cs b/src/UI/IUserFactory.cs new file mode 100644 index 0000000000..92f004b1ef --- /dev/null +++ b/src/UI/IUserFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.UI +{ + /// + /// Provides an abstraction for instantiating the given user type. + /// + /// The type of user. + public interface IUserFactory where TUser : class + { + /// + /// Creates an instance of a user and assigns the provided values. + /// + /// Email address + /// User name + /// Created user + TUser CreateUser(string email, string userName); + } +} diff --git a/src/UI/IdentityBuilderUIExtensions.cs b/src/UI/IdentityBuilderUIExtensions.cs index 33193dd8d6..d6be81b923 100644 --- a/src/UI/IdentityBuilderUIExtensions.cs +++ b/src/UI/IdentityBuilderUIExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Linq; using Microsoft.AspNetCore.Identity.UI; using Microsoft.AspNetCore.Identity.UI.Services; @@ -36,6 +37,13 @@ namespace Microsoft.AspNetCore.Identity .MakeGenericType(builder.UserType)); builder.Services.TryAddTransient(); + if (TryGetIdentityUserType(builder.UserType, out var primaryKeyType)) + { + var userFactoryType = typeof(IUserFactory<>).MakeGenericType(builder.UserType); + var defaultUserFactoryType = typeof(UserFactory<,>).MakeGenericType(builder.UserType, primaryKeyType); + builder.Services.TryAddSingleton(userFactoryType, defaultUserFactoryType); + } + return builder; } @@ -63,5 +71,24 @@ namespace Microsoft.AspNetCore.Identity } }); } + + private static bool TryGetIdentityUserType(Type userType, out Type primaryKeyType) + { + primaryKeyType = null; + + var baseType = userType.BaseType; + while (baseType != null) + { + if (baseType.IsGenericType && + baseType.GetGenericTypeDefinition() == typeof(IdentityUser<>)) + { + primaryKeyType = baseType.GetGenericArguments()[0]; + return true; + } + baseType = baseType.BaseType; + } + + return false; + } } } diff --git a/src/UI/IdentityDefaultUIConfigureOptions.cs b/src/UI/IdentityDefaultUIConfigureOptions.cs index 9d0f4effbc..ff39626995 100644 --- a/src/UI/IdentityDefaultUIConfigureOptions.cs +++ b/src/UI/IdentityDefaultUIConfigureOptions.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Identity.UI internal class IdentityDefaultUIConfigureOptions : IPostConfigureOptions, IPostConfigureOptions, - IPostConfigureOptions where TUser : IdentityUser + IPostConfigureOptions where TUser : class { private const string IdentityUIDefaultAreaName = "Identity"; diff --git a/src/UI/IdentityPageModelConvention.cs b/src/UI/IdentityPageModelConvention.cs index 1937c75b90..79635db72c 100644 --- a/src/UI/IdentityPageModelConvention.cs +++ b/src/UI/IdentityPageModelConvention.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace Microsoft.AspNetCore.Identity.UI { - internal class IdentityPageModelConvention : IPageApplicationModelConvention where TUser : IdentityUser + internal class IdentityPageModelConvention : IPageApplicationModelConvention where TUser : class { public void Apply(PageApplicationModel model) { @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Identity.UI private void ValidateTemplate(Type template) { - if(template.IsAbstract || !template.IsGenericTypeDefinition) + if (template.IsAbstract || !template.IsGenericTypeDefinition) { throw new InvalidOperationException("Implementation type can't be abstract or non generic."); } @@ -33,11 +33,6 @@ namespace Microsoft.AspNetCore.Identity.UI { throw new InvalidOperationException("Implementation type contains wrong generic arity."); } - var argument = genericArguments[0]; - if (!typeof(IdentityUser).IsAssignableFrom(typeof(TUser))) - { - throw new InvalidOperationException("Generic implementation type is not compatible."); - }; } } } diff --git a/src/UI/UserFactory.cs b/src/UI/UserFactory.cs new file mode 100644 index 0000000000..b56ce47498 --- /dev/null +++ b/src/UI/UserFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.UI +{ + internal class UserFactory : IUserFactory + where TUser : IdentityUser, new() + where TKey : IEquatable + { + public TUser CreateUser(string email, string userName) + { + return new TUser() { Email = email, UserName = userName }; + } + } +}