Identity: Add new email/confirmation flows (#8577)

This commit is contained in:
Hao Kung 2019-04-02 14:50:43 -07:00 committed by GitHub
parent 8274776efa
commit 555b506a97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1465 additions and 380 deletions

View File

@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Identity
}
public partial class SignInManager<TUser> where TUser : class
{
public SignInManager(Microsoft.AspNetCore.Identity.UserManager<TUser> userManager, Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> claimsFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<TUser>> logger, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes) { }
public SignInManager(Microsoft.AspNetCore.Identity.UserManager<TUser> userManager, Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> claimsFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<TUser>> logger, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, Microsoft.AspNetCore.Identity.IUserConfirmation<TUser> confirmation) { }
public Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> ClaimsFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.HttpContext Context { get { throw null; } set { } }
public virtual Microsoft.Extensions.Logging.ILogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -88,6 +88,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>>();

View File

@ -31,12 +31,14 @@ namespace Microsoft.AspNetCore.Identity
/// <param name="optionsAccessor">The accessor used to access the <see cref="IdentityOptions"/>.</param>
/// <param name="logger">The logger used to log messages, warnings and errors.</param>
/// <param name="schemes">The scheme provider that is used enumerate the authentication schemes.</param>
/// <param name="confirmation">The <see cref="IUserConfirmation{TUser}"/> used check whether a user account is confirmed.</param>
public SignInManager(UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes)
IAuthenticationSchemeProvider schemes,
IUserConfirmation<TUser> confirmation)
{
if (userManager == null)
{
@ -57,11 +59,13 @@ namespace Microsoft.AspNetCore.Identity
Options = optionsAccessor?.Value ?? new IdentityOptions();
Logger = logger;
_schemes = schemes;
_confirmation = confirmation;
}
private readonly IHttpContextAccessor _contextAccessor;
private HttpContext _context;
private IAuthenticationSchemeProvider _schemes;
private IUserConfirmation<TUser> _confirmation;
/// <summary>
/// Gets the <see cref="ILogger"/> used to log messages from the manager.
@ -148,7 +152,11 @@ namespace Microsoft.AspNetCore.Identity
Logger.LogWarning(1, "User {userId} cannot sign in without a confirmed phone number.", await UserManager.GetUserIdAsync(user));
return false;
}
if (Options.SignIn.RequireConfirmedAccount && !(await _confirmation.IsConfirmedAsync(UserManager, user)))
{
Logger.LogWarning(4, "User {userId} cannot sign in without a confirmed account.", await UserManager.GetUserIdAsync(user));
return false;
}
return true;
}

View File

@ -26,6 +26,12 @@ namespace Microsoft.AspNetCore.Identity
public virtual string Protect(string data) { throw null; }
public virtual string Unprotect(string data) { throw null; }
}
public partial class DefaultUserConfirmation<TUser> : Microsoft.AspNetCore.Identity.IUserConfirmation<TUser> where TUser : class
{
public DefaultUserConfirmation() { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public virtual System.Threading.Tasks.Task<bool> IsConfirmedAsync(Microsoft.AspNetCore.Identity.UserManager<TUser> manager, TUser user) { throw null; }
}
public partial class EmailTokenProvider<TUser> : Microsoft.AspNetCore.Identity.TotpSecurityStampBasedTokenProvider<TUser> where TUser : class
{
public EmailTokenProvider() { }
@ -194,6 +200,10 @@ namespace Microsoft.AspNetCore.Identity
System.Threading.Tasks.Task RemoveClaimsAsync(TUser user, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> claims, System.Threading.CancellationToken cancellationToken);
System.Threading.Tasks.Task ReplaceClaimAsync(TUser user, System.Security.Claims.Claim claim, System.Security.Claims.Claim newClaim, System.Threading.CancellationToken cancellationToken);
}
public partial interface IUserConfirmation<TUser> where TUser : class
{
System.Threading.Tasks.Task<bool> IsConfirmedAsync(Microsoft.AspNetCore.Identity.UserManager<TUser> manager, TUser user);
}
public partial interface IUserEmailStore<TUser> : Microsoft.AspNetCore.Identity.IUserStore<TUser>, System.IDisposable where TUser : class
{
System.Threading.Tasks.Task<TUser> FindByEmailAsync(string normalizedEmail, System.Threading.CancellationToken cancellationToken);
@ -396,6 +406,7 @@ namespace Microsoft.AspNetCore.Identity
public partial class SignInOptions
{
public SignInOptions() { }
public bool RequireConfirmedAccount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool RequireConfirmedEmail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool RequireConfirmedPhoneNumber { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}

View File

@ -0,0 +1,29 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Default implementation of <see cref="IUserConfirmation{TUser}"/>.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public class DefaultUserConfirmation<TUser> : IUserConfirmation<TUser> where TUser : class
{
/// <summary>
/// Determines whether the specified <paramref name="user"/> is confirmed.
/// </summary>
/// <param name="manager">The <see cref="UserManager{TUser}"/> that can be used to retrieve user properties.</param>
/// <param name="user">The user.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the confirmation operation.</returns>
public async virtual Task<bool> IsConfirmedAsync(UserManager<TUser> manager, TUser user)
{
if (!await manager.IsEmailConfirmedAsync(user))
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides an abstraction for confirmation of user accounts.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public interface IUserConfirmation<TUser> where TUser : class
{
/// <summary>
/// Determines whether the specified <paramref name="user"/> is confirmed.
/// </summary>
/// <param name="manager">The <see cref="UserManager{TUser}"/> that can be used to retrieve user properties.</param>
/// <param name="user">The user.</param>
/// <returns>Whether the user is confirmed.</returns>
Task<bool> IsConfirmedAsync(UserManager<TUser> manager, TUser user);
}
}

View File

@ -41,6 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();

View File

@ -19,5 +19,11 @@ namespace Microsoft.AspNetCore.Identity
/// </summary>
/// <value>True if a user must have a confirmed telephone number before they can sign in, otherwise false.</value>
public bool RequireConfirmedPhoneNumber { get; set; }
/// <summary>
/// Gets or sets a flag indicating whether a confirmed <see cref="IUserConfirmation{TUser}"/> account is required to sign in. Defaults to false.
/// </summary>
/// <value>True if a user must have a confirmed account before they can sign in, otherwise false.</value>
public bool RequireConfirmedAccount { get; set; }
}
}
}

View File

@ -1710,6 +1710,34 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
}
/// <summary>
/// Test.
/// </summary>
/// <returns>Task</returns>
[Fact]
public async Task CanChangeEmailOnlyIfEmailSame()
{
if (ShouldSkipDbTests())
{
return;
}
var manager = CreateManager();
var user = CreateTestUser("foouser");
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
var email = await manager.GetUserNameAsync(user) + "@diddly.bop";
IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email));
Assert.False(await manager.IsEmailConfirmedAsync(user));
var stamp = await manager.GetSecurityStampAsync(user);
var newEmail = await manager.GetUserNameAsync(user) + "@en.vec";
var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail);
var token2 = await manager.GenerateChangeEmailTokenAsync(user, "should@fail.com");
IdentityResultAssert.IsSuccess(await manager.ChangeEmailAsync(user, newEmail, token1));
Assert.True(await manager.IsEmailConfirmedAsync(user));
Assert.Equal(await manager.GetEmailAsync(user), newEmail);
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, "should@fail.com", token2));
}
/// <summary>
/// Test.
/// </summary>

View File

@ -37,9 +37,19 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
public void OnGet() { }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ConfirmEmailChangeModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ConfirmEmailChangeModel() { }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string userId, string email, string code) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ConfirmEmailModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ConfirmEmailModel() { }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string userId, string code) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
@ -103,6 +113,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
public string ReturnUrl { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task OnGetAsync(string returnUrl = null) { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync(string returnUrl = null) { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostSendVerificationEmailAsync() { throw null; }
public partial class InputModel
{
public InputModel() { }
@ -165,6 +176,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPost(string returnUrl = null) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public partial class RegisterConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public RegisterConfirmationModel() { }
public bool DisplayConfirmAccountLink { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string EmailConfirmationUrl { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string email) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class RegisterModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected RegisterModel() { }
@ -284,6 +304,27 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
public virtual Microsoft.AspNetCore.Mvc.IActionResult OnGet() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
}
public abstract partial class EmailModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected EmailModel() { }
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal.EmailModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool IsEmailConfirmed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostSendVerificationEmailAsync() { throw null; }
public partial class InputModel
{
public InputModel() { }
[System.ComponentModel.DataAnnotations.DisplayAttribute(Name="New email")]
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
[System.ComponentModel.DataAnnotations.RequiredAttribute]
public string NewEmail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
public partial class EnableAuthenticatorModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public EnableAuthenticatorModel() { }
@ -335,19 +376,14 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
protected IndexModel() { }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal.IndexModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool IsEmailConfirmed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Username { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostSendVerificationEmailAsync() { throw null; }
public partial class InputModel
{
public InputModel() { }
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
[System.ComponentModel.DataAnnotations.RequiredAttribute]
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.ComponentModel.DataAnnotations.DisplayAttribute(Name="Phone number")]
[System.ComponentModel.DataAnnotations.PhoneAttribute]
public string PhoneNumber { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
@ -358,6 +394,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
public static string ChangePassword { get { throw null; } }
public static string DeletePersonalData { get { throw null; } }
public static string DownloadPersonalData { get { throw null; } }
public static string Email { get { throw null; } }
public static string ExternalLogins { get { throw null; } }
public static string Index { get { throw null; } }
public static string PersonalData { get { throw null; } }
@ -365,6 +402,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
public static string ChangePasswordNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string DeletePersonalDataNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string DownloadPersonalDataNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string EmailNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string ExternalLoginsNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string IndexNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string PageNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, string page) { throw null; }
@ -450,9 +488,19 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
public void OnGet() { }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ConfirmEmailChangeModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ConfirmEmailChangeModel() { }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string userId, string email, string code) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ConfirmEmailModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ConfirmEmailModel() { }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string userId, string code) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
@ -578,6 +626,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPost(string returnUrl = null) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public partial class RegisterConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public RegisterConfirmationModel() { }
public bool DisplayConfirmAccountLink { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string EmailConfirmationUrl { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync(string email) { throw null; }
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class RegisterModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected RegisterModel() { }
@ -697,6 +754,27 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
public virtual Microsoft.AspNetCore.Mvc.IActionResult OnGet() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
}
public abstract partial class EmailModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected EmailModel() { }
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal.EmailModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool IsEmailConfirmed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostSendVerificationEmailAsync() { throw null; }
public partial class InputModel
{
public InputModel() { }
[System.ComponentModel.DataAnnotations.DisplayAttribute(Name="New email")]
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
[System.ComponentModel.DataAnnotations.RequiredAttribute]
public string NewEmail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
public partial class EnableAuthenticatorModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public EnableAuthenticatorModel() { }
@ -748,19 +826,14 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
protected IndexModel() { }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal.IndexModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool IsEmailConfirmed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.TempDataAttribute]
public string StatusMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Username { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnGetAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostSendVerificationEmailAsync() { throw null; }
public partial class InputModel
{
public InputModel() { }
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
[System.ComponentModel.DataAnnotations.RequiredAttribute]
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.ComponentModel.DataAnnotations.DisplayAttribute(Name="Phone number")]
[System.ComponentModel.DataAnnotations.PhoneAttribute]
public string PhoneNumber { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
@ -771,6 +844,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
public static string ChangePassword { get { throw null; } }
public static string DeletePersonalData { get { throw null; } }
public static string DownloadPersonalData { get { throw null; } }
public static string Email { get { throw null; } }
public static string ExternalLogins { get { throw null; } }
public static string Index { get { throw null; } }
public static string PersonalData { get { throw null; } }
@ -778,6 +852,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
public static string ChangePasswordNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string DeletePersonalDataNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string DownloadPersonalDataNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string EmailNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string ExternalLoginsNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string IndexNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext) { throw null; }
public static string PageNavClass(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, string page) { throw null; }

View File

@ -5,8 +5,4 @@
}
<h2>@ViewData["Title"]</h2>
<div>
<p>
Thank you for confirming your email.
</p>
</div>
<partial name="_StatusMessage" model="Model.StatusMessage" />

View File

@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
@ -18,6 +17,13 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
[IdentityDefaultUI(typeof(ConfirmEmailModel<>))]
public abstract class ConfirmEmailModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -48,11 +54,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
return Page();
}
}

View File

@ -0,0 +1,8 @@
@page
@model ConfirmEmailChangeModel
@{
ViewData["Title"] = "Confirm email change";
}
<h2>@ViewData["Title"]</h2>
<partial name="_StatusMessage" model="Model.StatusMessage" />

View File

@ -0,0 +1,80 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
[IdentityDefaultUI(typeof(ConfirmEmailChangeModel<>))]
public abstract class ConfirmEmailChangeModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync(string userId, string email, string code) => throw new NotImplementedException();
}
internal class ConfirmEmailChangeModel<TUser> : ConfirmEmailChangeModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
public ConfirmEmailChangeModel(UserManager<TUser> userManager, SignInManager<TUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
public override async Task<IActionResult> OnGetAsync(string userId, string email, string code)
{
if (userId == null || email == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ChangeEmailAsync(user, email, code);
if (!result.Succeeded)
{
StatusMessage = "Error changing email.";
return Page();
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
if (!setUserNameResult.Succeeded)
{
StatusMessage = "Error changing user name.";
return Page();
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Thank you for confirming your email change.";
return Page();
}
}
}

View File

@ -32,7 +32,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button id="login-submit" type="submit" class="btn btn-default">Log in</button>
</div>
<div class="form-group">
<p>
@ -41,6 +41,9 @@
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
</p>
<p>
<button id="resend-confirmation" type="submit" asp-page-handler="SendVerificationEmail" class="btn-link" style="padding:0px;margin:0px;border:0px">Resend email confirmation</button>
</p>
</div>
</form>
</section>

View File

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
@ -90,16 +92,28 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostAsync(string returnUrl = null) => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostSendVerificationEmailAsync() => throw new NotImplementedException();
}
internal class LoginModel<TUser> : LoginModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IEmailSender _emailSender;
public LoginModel(SignInManager<TUser> signInManager, ILogger<LoginModel> logger)
public LoginModel(SignInManager<TUser> signInManager, ILogger<LoginModel> logger,
UserManager<TUser> userManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
}
@ -155,5 +169,34 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
// If we got this far, something failed, redisplay form
return Page();
}
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.FindByEmailAsync(Input.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
}
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.Email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
return Page();
}
}
}

View File

@ -0,0 +1,41 @@
@page
@model EmailModel
@{
ViewData["Title"] = "Manage Email";
ViewData["ActivePage"] = ManageNavPages.Email;
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" model="Model.StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="email-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Email" class="form-control" disabled />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Email" class="form-control" disabled />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
</div>
<div class="form-group">
<label asp-for="Input.NewEmail"></label>
<input asp-for="Input.NewEmail" class="form-control" />
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
</div>
<button id="change-email-button" type="submit" class="btn btn-default">Change email</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,185 @@
// 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.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[IdentityDefaultUI(typeof(EmailModel<>))]
public abstract class EmailModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string NewEmail { get; set; }
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostSendVerificationEmailAsync() => throw new NotImplementedException();
}
internal class EmailModel<TUser> : EmailModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly IEmailSender _emailSender;
public EmailModel(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public override async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
Email = email;
Input = new InputModel
{
NewEmail = email,
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
public override async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
if (Input.NewEmail != email)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
var callbackUrl = Url.Page(
"/Account/ConfirmEmailChange",
pageHandler: null,
values: new { userId = userId, email = Input.NewEmail, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.NewEmail,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
}
StatusMessage = "Your email is unchanged.";
return RedirectToPage();
}
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
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 = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -15,22 +15,6 @@
<label asp-for="Username"></label>
<input asp-for="Username" class="form-control" disabled />
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />

View File

@ -3,9 +3,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@ -24,12 +22,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
/// </summary>
public string Username { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -50,14 +42,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -78,28 +62,19 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostSendVerificationEmailAsync() => throw new NotImplementedException();
}
internal class IndexModel<TUser> : IndexModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly IEmailSender _emailSender;
public IndexModel(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
IEmailSender emailSender)
SignInManager<TUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public override async Task<IActionResult> OnGetAsync()
@ -110,19 +85,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.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 = userName;
Input = new InputModel
{
Email = email,
PhoneNumber = phoneNumber
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
@ -139,26 +110,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
if (Input.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await _userManager.SetUserNameAsync(user, Input.Email);
if (!setUserNameResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting name for user with ID '{userId}'.");
}
}
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (Input.PhoneNumber != phoneNumber)
{
@ -174,36 +125,5 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
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 = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -18,6 +18,12 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
/// </summary>
public static string Index => "Index";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string Email => "Email";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -60,6 +66,12 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Manage.Internal
/// </summary>
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.

View File

@ -1,5 +1,6 @@
<ul class="nav nav-pills nav-stacked">
<ul class="nav nav-pills nav-stacked">
<li class="@ManageNavPages.IndexNavClass(ViewContext)"><a id="profile" asp-page="./Index">Profile</a></li>
<li class="@ManageNavPages.EmailNavClass(ViewContext)"><a id="email" asp-page="./Email">Email</a></li>
<li class="@ManageNavPages.ChangePasswordNavClass(ViewContext)"><a id="change-password" asp-page="./ChangePassword">Password</a></li>
@if ((bool)ViewData["ManageNav.HasExternalLogins"])
{

View File

@ -150,8 +150,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
}
else
{
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{

View File

@ -0,0 +1,23 @@
@page
@model RegisterConfirmationModel
@{
ViewData["Title"] = "Register confirmation";
}
<h2>@ViewData["Title"]</h2>
@{
if (@Model.DisplayConfirmAccountLink)
{
<p>
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
</p>
}
else
{
<p>
Please check your email to confirm your account.
</p>
}
}

View File

@ -0,0 +1,92 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
[IdentityDefaultUI(typeof(RegisterConfirmationModel<>))]
public class RegisterConfirmationModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool DisplayConfirmAccountLink { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string EmailConfirmationUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync(string email) => throw new NotImplementedException();
}
internal class RegisterConfirmationModel<TUser> : RegisterConfirmationModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly IWebHostEnvironment _hostingEnv;
private readonly IEmailSender _sender;
public RegisterConfirmationModel(UserManager<TUser> userManager, IWebHostEnvironment hostingEnv, IEmailSender sender)
{
_userManager = userManager;
_hostingEnv = hostingEnv;
_sender = sender;
}
public override async Task<IActionResult> OnGetAsync(string email)
{
if (email == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return NotFound($"Unable to load user with email '{email}'.");
}
Email = email;
// If the email sender is a no-op, display the confirm link in the page
DisplayConfirmAccountLink = _sender is EmailSender;
if (DisplayConfirmAccountLink)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
EmailConfirmationUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
}
return Page();
}
}
}

View File

@ -0,0 +1,10 @@
@model string
@if (!String.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@Model
</div>
}

View File

@ -5,8 +5,3 @@
}
<h1>@ViewData["Title"]</h1>
<div>
<p>
Thank you for confirming your email.
</p>
</div>

View File

@ -17,6 +17,13 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
[IdentityDefaultUI(typeof(ConfirmEmailModel<>))]
public abstract class ConfirmEmailModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -47,11 +54,7 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
return Page();
}
}

View File

@ -0,0 +1,8 @@
@page
@model ConfirmEmailChangeModel
@{
ViewData["Title"] = "Confirm email change";
}
<h2>@ViewData["Title"]</h2>
<partial name="_StatusMessage" model="Model.StatusMessage" />

View File

@ -0,0 +1,79 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
[IdentityDefaultUI(typeof(ConfirmEmailChangeModel<>))]
public abstract class ConfirmEmailChangeModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync(string userId, string email, string code) => throw new NotImplementedException();
}
internal class ConfirmEmailChangeModel<TUser> : ConfirmEmailChangeModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
public ConfirmEmailChangeModel(UserManager<TUser> userManager, SignInManager<TUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
public override async Task<IActionResult> OnGetAsync(string userId, string email, string code)
{
if (userId == null || email == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ChangeEmailAsync(user, email, code);
if (!result.Succeeded)
{
StatusMessage = "Error changing email.";
return Page();
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
if (!setUserNameResult.Succeeded)
{
StatusMessage = "Error changing user name.";
return Page();
}
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Thank you for confirming your email change.";
return Page();
}
}
}

View File

@ -32,7 +32,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Log in</button>
<button id="login-submit" type="submit" class="btn btn-primary">Log in</button>
</div>
<div class="form-group">
<p>

View File

@ -0,0 +1,41 @@
@page
@model EmailModel
@{
ViewData["Title"] = "Manage Email";
ViewData["ActivePage"] = ManageNavPages.Email;
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<form id="email-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Email" class="form-control" disabled />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Email" class="form-control" disabled />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
</div>
<div class="form-group">
<label asp-for="Input.NewEmail"></label>
<input asp-for="Input.NewEmail" class="form-control" />
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
</div>
<button id="change-email-button" type="submit" class="btn btn-default">Change email</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,185 @@
// 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.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[IdentityDefaultUI(typeof(EmailModel<>))]
public abstract class EmailModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[TempData]
public string StatusMessage { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[BindProperty]
public InputModel Input { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string NewEmail { get; set; }
}
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostSendVerificationEmailAsync() => throw new NotImplementedException();
}
internal class EmailModel<TUser> : EmailModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly IEmailSender _emailSender;
public EmailModel(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public override async Task<IActionResult> OnGetAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
Email = email;
Input = new InputModel
{
NewEmail = email,
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
public override async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
if (Input.NewEmail != email)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
var callbackUrl = Url.Page(
"/Account/ConfirmEmailChange",
pageHandler: null,
values: new { userId = userId, email = Input.NewEmail, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.NewEmail,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
}
StatusMessage = "Your email is unchanged.";
return RedirectToPage();
}
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
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 = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -15,22 +15,6 @@
<label asp-for="Username"></label>
<input asp-for="Username" class="form-control" disabled />
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />

View File

@ -24,12 +24,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
/// </summary>
public string Username { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool IsEmailConfirmed { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -50,14 +44,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
/// </summary>
public class InputModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -78,28 +64,19 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnPostSendVerificationEmailAsync() => throw new NotImplementedException();
}
internal class IndexModel<TUser> : IndexModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly IEmailSender _emailSender;
public IndexModel(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
IEmailSender emailSender)
SignInManager<TUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public override async Task<IActionResult> OnGetAsync()
@ -110,19 +87,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.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 = userName;
Input = new InputModel
{
Email = email,
PhoneNumber = phoneNumber
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
@ -139,26 +112,6 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
if (Input.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
}
// In our UI email and user name are one and the same, so when we update the email
// we need to update the user name.
var setUserNameResult = await _userManager.SetUserNameAsync(user, Input.Email);
if (!setUserNameResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting name for user with ID '{userId}'.");
}
}
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (Input.PhoneNumber != phoneNumber)
{
@ -174,36 +127,5 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
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 = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -18,6 +18,12 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
/// </summary>
public static string Index => "Index";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string Email => "Email";
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
@ -60,6 +66,12 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Manage.Internal
/// </summary>
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.

View File

@ -1,5 +1,6 @@
<ul class="nav nav-pills flex-column">
<ul class="nav nav-pills flex-column">
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
@if ((bool)ViewData["ManageNav.HasExternalLogins"])
{

View File

@ -149,8 +149,15 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
}
else
{
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{

View File

@ -0,0 +1,23 @@
@page
@model RegisterConfirmationModel
@{
ViewData["Title"] = "Register confirmation";
}
<h2>@ViewData["Title"]</h2>
@{
if (@Model.DisplayConfirmAccountLink)
{
<p>
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
</p>
}
else
{
<p>
Please check your email to confirm your account.
</p>
}
}

View File

@ -0,0 +1,93 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
[AllowAnonymous]
[IdentityDefaultUI(typeof(RegisterConfirmationModel<>))]
public class RegisterConfirmationModel : PageModel
{
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string Email { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool DisplayConfirmAccountLink { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public string EmailConfirmationUrl { get; set; }
/// <summary>
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Task<IActionResult> OnGetAsync(string email) => throw new NotImplementedException();
}
internal class RegisterConfirmationModel<TUser> : RegisterConfirmationModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly IWebHostEnvironment _hostingEnv;
private readonly IEmailSender _sender;
public RegisterConfirmationModel(UserManager<TUser> userManager, IWebHostEnvironment hostingEnv, IEmailSender sender)
{
_userManager = userManager;
_hostingEnv = hostingEnv;
_sender = sender;
}
public override async Task<IActionResult> OnGetAsync(string email)
{
if (email == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return NotFound($"Unable to load user with email '{email}'.");
}
Email = email;
// This sender is a no-op, so we should auto confirm the account
DisplayConfirmAccountLink = _sender is EmailSender;
if (DisplayConfirmAccountLink)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
EmailConfirmationUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
}
return Page();
}
}
}

View File

@ -0,0 +1,10 @@
@model string
@if (!String.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@Model
</div>
}

View File

@ -22,22 +22,6 @@
<label asp-for="Input.Age"></label>
<input asp-for="Input.Age" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />

View File

@ -3,11 +3,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using IdentitySample.DefaultUI.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@ -17,22 +15,17 @@ namespace IdentitySample.DefaultUI
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
public IndexModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender)
SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
public string Username { get; set; }
public bool IsEmailConfirmed { get; set; }
[TempData]
public string StatusMessage { get; set; }
@ -41,10 +34,6 @@ namespace IdentitySample.DefaultUI
public class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
@ -73,12 +62,9 @@ namespace IdentitySample.DefaultUI
{
Name = user.Name,
Age = user.Age,
Email = user.Email,
PhoneNumber = user.PhoneNumber
};
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
return Page();
}
@ -111,15 +97,6 @@ namespace IdentitySample.DefaultUI
throw new InvalidOperationException($"Unexpected error ocurred updating the profile for user with ID '{user.Id}'");
}
if (Input.Email != user.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}'.");
}
}
if (Input.PhoneNumber != user.PhoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
@ -133,33 +110,5 @@ namespace IdentitySample.DefaultUI
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { user.Id, code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
user.Email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}
}

View File

@ -106,8 +106,15 @@ namespace IdentitySample.DefaultUI
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
}
else
{
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{

View File

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using IdentitySample.DefaultUI.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -11,6 +12,14 @@ using Microsoft.Extensions.Logging;
namespace IdentitySample.DefaultUI
{
public class BadDude : IUserConfirmation<ApplicationUser>
{
public Task<bool> IsConfirmedAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
{
return Task.FromResult(false);
}
}
public class Startup
{
public Startup(IWebHostEnvironment env)
@ -37,7 +46,7 @@ namespace IdentitySample.DefaultUI
services.AddMvc().AddNewtonsoftJson();
services.AddDefaultIdentity<ApplicationUser>()
services.AddDefaultIdentity<ApplicationUser>(o => o.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -35,6 +35,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
internal DefaultUIContext WithCookieConsent() =>
new DefaultUIContext(this) { CookiePolicyAccepted = true };
internal DefaultUIContext WithRealEmailSender() =>
new DefaultUIContext(this) { HasRealEmailSender = true };
public string AuthenticatorKey
{
get => GetValue<string>(nameof(AuthenticatorKey));
@ -93,5 +96,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
get => GetValue<bool>(nameof(CookiePolicyAccepted));
set => SetValue(nameof(CookiePolicyAccepted), value);
}
public bool HasRealEmailSender
{
get => GetValue<bool>(nameof(HasRealEmailSender));
set => SetValue(nameof(HasRealEmailSender), value);
}
}
}

View File

@ -70,9 +70,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
void ConfigureTestServices(IServiceCollection services) =>
services.SetupTestEmailSender(emails);
var client = ServerFactory
.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices))
.CreateClient();
var server = ServerFactory
.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
var client = server.CreateClient();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
@ -91,21 +91,30 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
{
// Arrange
var emails = new ContosoEmailSender();
var client = ServerFactory
.CreateClient();
void ConfigureTestServices(IServiceCollection services) =>
services.SetupTestEmailSender(emails);
var server = ServerFactory
.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
var client = server.CreateClient();
var newClient = server.CreateClient();
var failedClient = server.CreateClient();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
var newEmail = "updatedEmail@example.com";
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
var manageIndex = await UserStories.SendUpdateProfileAsync(index, newEmail);
var email = await UserStories.SendUpdateEmailAsync(index, newEmail);
// Act & Assert
var pageUserName = manageIndex.GetUserName();
Assert.Equal(newEmail, pageUserName);
var pageEmail = manageIndex.GetEmail();
Assert.Equal(newEmail, pageEmail);
Assert.Equal(2, emails.SentEmails.Count);
await UserStories.ConfirmEmailAsync(emails.SentEmails[1], client);
// Verify can login with new email, fails with old
await UserStories.LoginExistingUserAsync(newClient, newEmail, password);
await UserStories.LoginFailsWithWrongPasswordAsync(failedClient, userName, password);
}
[Fact]

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Net;
@ -6,9 +6,7 @@ using System.Threading.Tasks;
using Identity.DefaultUI.WebSite;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Identity.FunctionalTests
{

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Collections.Generic;
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
private readonly IHtmlAnchorElement _forgotPasswordLink;
private readonly IHtmlFormElement _externalLoginForm;
private readonly IHtmlElement _contosoButton;
private readonly IHtmlElement _loginButton;
public Login(
HttpClient client,
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
: base(client, login, context)
{
_loginForm = HtmlAssert.HasForm("#account", login);
_loginButton = HtmlAssert.HasElement("#login-submit", login);
_forgotPasswordLink = HtmlAssert.HasLink("#forgot-password", login);
if (Context.ContosoLoginEnabled)
{
@ -87,7 +89,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
private async Task<HttpResponseMessage> SendLoginForm(string userName, string password)
{
return await Client.SendAsync(_loginForm, new Dictionary<string, string>()
return await Client.SendAsync(_loginForm, _loginButton, new Dictionary<string, string>()
{
["Input_Email"] = userName,
["Input_Password"] = password

View File

@ -0,0 +1,67 @@
// 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.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
{
public class Email : DefaultUIPage
{
private readonly IHtmlElement _emailInput;
private readonly IHtmlElement _newEmailInput;
private readonly IHtmlElement _confirmEmailButton;
private readonly IHtmlFormElement _changeEmailForm;
private readonly IHtmlElement _changeEmailButton;
public static readonly string Path = "/";
public Email(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
: base(client, manage, context)
{
Assert.True(Context.UserAuthenticated);
_changeEmailForm = HtmlAssert.HasForm("#email-form", manage);
_emailInput = HtmlAssert.HasElement("#Email", manage);
_newEmailInput = HtmlAssert.HasElement("#Input_NewEmail", manage);
_changeEmailButton = HtmlAssert.HasElement("#change-email-button", manage);
if (!Context.EmailConfirmed)
{
_confirmEmailButton = HtmlAssert.HasElement("button#email-verification", manage);
}
}
internal async Task<Email> SendConfirmationEmailAsync()
{
Assert.False(Context.EmailConfirmed);
var response = await Client.SendAsync(_changeEmailForm, _confirmEmailButton);
var goToManage = ResponseAssert.IsRedirect(response);
var manageResponse = await Client.GetAsync(goToManage);
var manage = await ResponseAssert.IsHtmlDocumentAsync(manageResponse);
return new Email(Client, manage, Context);
}
internal async Task<Email> SendUpdateEmailAsync(string newEmail)
{
var response = await Client.SendAsync(_changeEmailForm, _changeEmailButton, new Dictionary<string, string>
{
["Input_NewEmail"] = newEmail
});
var goToManage = ResponseAssert.IsRedirect(response);
var manageResponse = await Client.GetAsync(goToManage);
var manage = await ResponseAssert.IsHtmlDocumentAsync(manageResponse);
return new Email(Client, manage, Context);
}
internal object GetEmail() => _emailInput.GetAttribute("value");
internal object GetNewEmail() => _newEmailInput.GetAttribute("value");
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -13,15 +13,14 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
public class Index : DefaultUIPage
{
private readonly IHtmlAnchorElement _profileLink;
private readonly IHtmlAnchorElement _emailLink;
private readonly IHtmlAnchorElement _changePasswordLink;
private readonly IHtmlAnchorElement _twoFactorLink;
private readonly IHtmlAnchorElement _externalLoginLink;
private readonly IHtmlAnchorElement _personalDataLink;
private readonly IHtmlFormElement _updateProfileForm;
private readonly IHtmlElement _emailInput;
private readonly IHtmlElement _userNameInput;
private readonly IHtmlElement _updateProfileButton;
private readonly IHtmlElement _confirmEmailButton;
public static readonly string Path = "/";
public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
@ -30,6 +29,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
Assert.True(Context.UserAuthenticated);
_profileLink = HtmlAssert.HasLink("#profile", manage);
_emailLink = HtmlAssert.HasLink("#email", manage);
_changePasswordLink = HtmlAssert.HasLink("#change-password", manage);
_twoFactorLink = HtmlAssert.HasLink("#two-factor", manage);
if (Context.ContosoLoginEnabled)
@ -38,13 +38,8 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
}
_personalDataLink = HtmlAssert.HasLink("#personal-data", manage);
_updateProfileForm = HtmlAssert.HasForm("#profile-form", manage);
_emailInput = HtmlAssert.HasElement("#Input_Email", manage);
_userNameInput = HtmlAssert.HasElement("#Username", manage);
_updateProfileButton = HtmlAssert.HasElement("#update-profile-button", manage);
if (!Context.EmailConfirmed)
{
_confirmEmailButton = HtmlAssert.HasElement("button#email-verification", manage);
}
}
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync(bool consent = true)
@ -71,31 +66,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
return new TwoFactorAuthentication(Client, twoFactor, Context);
}
internal async Task<Index> SendConfirmationEmailAsync()
{
Assert.False(Context.EmailConfirmed);
var response = await Client.SendAsync(_updateProfileForm, _confirmEmailButton);
var goToManage = ResponseAssert.IsRedirect(response);
var manageResponse = await Client.GetAsync(goToManage);
var manage = await ResponseAssert.IsHtmlDocumentAsync(manageResponse);
return new Index(Client, manage, Context);
}
internal async Task<Index> SendUpdateProfileAsync(string newEmail)
{
var response = await Client.SendAsync(_updateProfileForm, _updateProfileButton, new Dictionary<string, string>
{
["Input_Email"] = newEmail
});
var goToManage = ResponseAssert.IsRedirect(response);
var manageResponse = await Client.GetAsync(goToManage);
var manage = await ResponseAssert.IsHtmlDocumentAsync(manageResponse);
return new Index(Client, manage, Context);
}
public async Task<ChangePassword> ClickChangePasswordLinkAsync()
{
var goToChangePassword = await Client.GetAsync(_changePasswordLink.Href);
@ -103,8 +73,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
return new ChangePassword(Client, changePasswordDocument, Context);
}
internal string GetEmail() => _emailInput.GetAttribute("value");
internal object GetUserName() => _userNameInput.GetAttribute("value");
public async Task<SetPassword> ClickChangePasswordLinkExternalLoginAsync()
@ -123,6 +91,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
return new PersonalData(Client, personalData, Context);
}
public async Task<Email> ClickEmailLinkAsync()
{
var goToEmail = await Client.GetAsync(_emailLink.Href);
var email = await ResponseAssert.IsHtmlDocumentAsync(goToEmail);
return new Email(Client, email, Context);
}
public async Task<LinkExternalLogin> ClickLinkLoginAsync()
{
var goToExternalLogin = await Client.GetAsync(_externalLoginLink.Href);

View File

@ -53,5 +53,22 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
return new Index(Client, index, Context.WithAuthenticatedUser());
}
public async Task<RegisterConfirmation> SubmitRegisterFormWithConfirmation(string userName, string password, bool hasRealEmail = false)
{
var registered = await Client.SendAsync(_registerForm, new Dictionary<string, string>()
{
["Input_Email"] = userName,
["Input_Password"] = password,
["Input_ConfirmPassword"] = password
});
var registeredLocation = ResponseAssert.IsRedirect(registered);
Assert.Equal(RegisterConfirmation.Path + "?email="+userName, registeredLocation.ToString());
var registerResponse = await Client.GetAsync(registeredLocation);
var register = await ResponseAssert.IsHtmlDocumentAsync(registerResponse);
return new RegisterConfirmation(Client, register, hasRealEmail ? Context.WithRealEmailSender() : Context);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Net.Http;

View File

@ -0,0 +1,41 @@
// 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.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using Microsoft.AspNetCore.Identity.FunctionalTests.Account;
using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests
{
public class RegisterConfirmation : DefaultUIPage
{
private readonly IHtmlAnchorElement _confirmLink;
public static readonly string Path = "/Identity/Account/RegisterConfirmation";
public RegisterConfirmation(
HttpClient client,
IHtmlDocument register,
DefaultUIContext context)
: base(client, register, context)
{
if (Context.HasRealEmailSender)
{
Assert.Empty(Document.QuerySelectorAll("#confirm-link"));
}
else
{
_confirmLink = HtmlAssert.HasLink("#confirm-link", Document);
}
}
public async Task<ConfirmEmail> ClickConfirmLinkAsync()
{
var goToConfirm = await Client.GetAsync(_confirmLink.Href);
var confirm = await ResponseAssert.IsHtmlDocumentAsync(goToConfirm);
return await ConfirmEmail.Create(_confirmLink, Client, Context);
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -37,6 +38,59 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
await UserStories.RegisterNewUserAsync(client, userName, password);
}
[Fact]
public async Task CanRegisterAUserWithRequiredConfirmation()
{
// Arrange
void ConfigureTestServices(IServiceCollection services) { services.Configure<IdentityOptions>(o => o.SignIn.RequireConfirmedAccount = true); };
var server = ServerFactory
.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
var client = server.CreateClient();
var client2 = server.CreateClient();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
// Act & Assert
var register = await UserStories.RegisterNewUserAsyncWithConfirmation(client, userName, password);
// Since we aren't confirmed yet, login should fail until we confirm
await UserStories.LoginFailsWithWrongPasswordAsync(client, userName, password);
await register.ClickConfirmLinkAsync();
await UserStories.LoginExistingUserAsync(client, userName, password);
}
private class FakeEmailSender : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string htmlMessage)
=> Task.CompletedTask;
}
[Fact]
public async Task RegisterWithRealConfirmationDoesNotShowLink()
{
// Arrange
void ConfigureTestServices(IServiceCollection services) {
services.Configure<IdentityOptions>(o => o.SignIn.RequireConfirmedAccount = true);
services.AddSingleton<IEmailSender, FakeEmailSender>();
};
var server = ServerFactory
.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
var client = server.CreateClient();
var client2 = server.CreateClient();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
// Act & Assert
var register = await UserStories.RegisterNewUserAsyncWithConfirmation(client, userName, password, hasRealEmailSender: true);
// Since we aren't confirmed yet, login should fail until we confirm
await UserStories.LoginFailsWithWrongPasswordAsync(client, userName, password);
}
[Fact]
public async Task CanRegisterAUser_WithGlobalAuthorizeFilter()
{

View File

@ -27,6 +27,17 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
return await register.SubmitRegisterFormForValidUserAsync(userName, password);
}
internal static async Task<RegisterConfirmation> RegisterNewUserAsyncWithConfirmation(HttpClient client, string userName = null, string password = null, bool hasRealEmailSender = false)
{
userName = userName ?? $"{Guid.NewGuid()}@example.com";
password = password ?? $"!Test.Password1$";
var index = await Index.CreateAsync(client);
var register = await index.ClickRegisterLinkAsync();
return await register.SubmitRegisterFormWithConfirmation(userName, password, hasRealEmailSender);
}
internal static async Task<Index> LoginExistingUserAsync(HttpClient client, string userName, string password)
{
@ -82,16 +93,18 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
return await externalLogin.SendEmailAsync(email);
}
internal static async Task<Account.Manage.Index> SendEmailConfirmationLinkAsync(Index index)
internal static async Task<Email> SendEmailConfirmationLinkAsync(Index index)
{
var manage = await index.ClickManageLinkAsync();
return await manage.SendConfirmationEmailAsync();
var email = await manage.ClickEmailLinkAsync();
return await email.SendConfirmationEmailAsync();
}
internal static async Task<Account.Manage.Index> SendUpdateProfileAsync(Index index, string newEmail)
internal static async Task<Email> SendUpdateEmailAsync(Index index, string newEmail)
{
var manage = await index.ClickManageLinkAsync();
return await manage.SendUpdateProfileAsync(newEmail);
var email = await manage.ClickEmailLinkAsync();
return await email.SendUpdateEmailAsync(newEmail);
}
internal static async Task<Index> LoginWithSocialLoginAsync(HttpClient client, string userName)

View File

@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Identity.Test
private class MySignInManager : SignInManager<PocoUser>
{
public MySignInManager(UserManager<PocoUser> manager, IHttpContextAccessor context, IUserClaimsPrincipalFactory<PocoUser> claimsFactory) : base(manager, context, claimsFactory, null, null, null) { }
public MySignInManager(UserManager<PocoUser> manager, IHttpContextAccessor context, IUserClaimsPrincipalFactory<PocoUser> claimsFactory) : base(manager, context, claimsFactory, null, null, null, null) { }
}
private class MyUserManager : UserManager<PocoUser>
@ -357,4 +357,4 @@ namespace Microsoft.AspNetCore.Identity.Test
}
}
}
}
}

View File

@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
var signInManager = new Mock<SignInManager<PocoUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(shouldStampValidate ? user : default).Verifiable();
if (shouldStampValidate)
@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
var signInManager = new SignInManager<PocoUser>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
userManager.Setup(u => u.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user).Verifiable();
claimsManager.Setup(c => c.CreateAsync(user)).ReturnsAsync(new ClaimsPrincipal()).Verifiable();
@ -205,7 +205,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
var signInManager = new Mock<SignInManager<PocoUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(default(PocoUser)).Verifiable();
var services = new ServiceCollection();
services.AddSingleton(options.Object);
@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
var signInManager = new Mock<SignInManager<PocoUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).Throws(new Exception("Shouldn't be called"));
signInManager.Setup(s => s.SignInAsync(user, false, null)).Throws(new Exception("Shouldn't be called"));
var services = new ServiceCollection();
@ -277,7 +277,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object);
var signInManager = new Mock<SignInManager<PocoUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
signInManager.Setup(s => s.ValidateTwoFactorSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(shouldStampValidate ? user : default).Verifiable();
var services = new ServiceCollection();
@ -309,4 +309,4 @@ namespace Microsoft.AspNetCore.Identity.Test
public Task TwoFactorRememberClientOnValidatePrincipalRejectsWhenValidateSecurityStampFails()
=> RunRememberClientCookieTest(shouldStampValidate: false, validationSuccess: false);
}
}
}

View File

@ -69,13 +69,13 @@ namespace Microsoft.AspNetCore.Identity.Test
[Fact]
public void ConstructorNullChecks()
{
Assert.Throws<ArgumentNullException>("userManager", () => new SignInManager<PocoUser>(null, null, null, null, null, null));
Assert.Throws<ArgumentNullException>("userManager", () => new SignInManager<PocoUser>(null, null, null, null, null, null, null));
var userManager = MockHelpers.MockUserManager<PocoUser>().Object;
Assert.Throws<ArgumentNullException>("contextAccessor", () => new SignInManager<PocoUser>(userManager, null, null, null, null, null));
Assert.Throws<ArgumentNullException>("contextAccessor", () => new SignInManager<PocoUser>(userManager, null, null, null, null, null, null));
var contextAccessor = new Mock<IHttpContextAccessor>();
var context = new Mock<HttpContext>();
contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
Assert.Throws<ArgumentNullException>("claimsFactory", () => new SignInManager<PocoUser>(userManager, contextAccessor.Object, null, null, null, null));
Assert.Throws<ArgumentNullException>("claimsFactory", () => new SignInManager<PocoUser>(userManager, contextAccessor.Object, null, null, null, null, null));
}
//[Fact]
@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Identity.Test
options.Setup(a => a.Value).Returns(identityOptions);
var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager.Object, roleManager.Object, options.Object);
var logger = new TestLogger<SignInManager<PocoUser>>();
var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object);
var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
// Act
var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false);
@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.Identity.Test
options.Setup(a => a.Value).Returns(identityOptions);
var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager.Object, roleManager.Object, options.Object);
var logger = new TestLogger<SignInManager<PocoUser>>();
var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object);
var helper = new SignInManager<PocoUser>(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger, new Mock<IAuthenticationSchemeProvider>().Object, new DefaultUserConfirmation<PocoUser>());
// Act
var result = await helper.CheckPasswordSignInAsync(user, "bogus", false);
@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.Identity.Test
options.Setup(a => a.Value).Returns(identityOptions);
var claimsFactory = new UserClaimsPrincipalFactory<PocoUser, PocoRole>(manager, roleManager.Object, options.Object);
schemeProvider = schemeProvider ?? new Mock<IAuthenticationSchemeProvider>().Object;
var sm = new SignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options.Object, null, schemeProvider);
var sm = new SignInManager<PocoUser>(manager, contextAccessor.Object, claimsFactory, options.Object, null, schemeProvider, new DefaultUserConfirmation<PocoUser>());
sm.Logger = logger ?? NullLogger<SignInManager<PocoUser>>.Instance;
return sm;
}
@ -575,7 +575,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var signInManager = new Mock<SignInManager<PocoUser>>(manager.Object,
new HttpContextAccessor { HttpContext = context },
new Mock<IUserClaimsPrincipalFactory<PocoUser>>().Object,
null, null, new Mock<IAuthenticationSchemeProvider>().Object)
null, null, new Mock<IAuthenticationSchemeProvider>().Object, null)
{ CallBase = true };
//signInManager.Setup(s => s.SignInAsync(user, It.Is<AuthenticationProperties>(p => p.IsPersistent == isPersistent),
//externalLogin? loginProvider : null)).Returns(Task.FromResult(0)).Verifiable();