Fix resend email confirmation (#14118)

This commit is contained in:
Hao Kung 2019-11-05 10:32:06 -08:00 committed by GitHub
parent b617083148
commit 41195138b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 384 additions and 2 deletions

View File

@ -207,6 +207,22 @@ namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
}
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ResendEmailConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ResendEmailConfirmationModel() { }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal.ResendEmailConfirmationModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public virtual void OnGet() { }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { 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 { } }
}
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public partial class ResetPasswordConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public ResetPasswordConfirmationModel() { }
@ -657,6 +673,22 @@ namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
}
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public abstract partial class ResendEmailConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
protected ResendEmailConfirmationModel() { }
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
public Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal.ResendEmailConfirmationModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public virtual void OnGet() { }
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { 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 { } }
}
}
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
public partial class ResetPasswordConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
{
public ResetPasswordConfirmationModel() { }

View File

@ -41,6 +41,9 @@
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
</p>
<p>
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>
</div>
</form>
</section>

View File

@ -0,0 +1,26 @@
@page
@model ResendEmailConfirmationModel
@{
ViewData["Title"] = "Resend email confirmation";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Resend</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,106 @@
// 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;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
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(ResendEmailConfirmationModel<>))]
public abstract class ResendEmailConfirmationModel : 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>
[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]
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 virtual void OnGet() => 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();
}
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly IEmailSender _emailSender;
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
public override void OnGet()
{
}
public override async Task<IActionResult> OnPostAsync()
{
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.");
return Page();
}
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
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

@ -41,6 +41,9 @@
<p>
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
</p>
<p>
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
</p>
</div>
</form>
</section>

View File

@ -0,0 +1,26 @@
@page
@model ResendEmailConfirmationModel
@{
ViewData["Title"] = "Resend email confirmation";
}
<h1>@ViewData["Title"]</h1>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Resend</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,106 @@
// 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;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
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(ResendEmailConfirmationModel<>))]
public abstract class ResendEmailConfirmationModel : 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>
[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]
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 virtual void OnGet() => 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();
}
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly IEmailSender _emailSender;
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
}
public override void OnGet()
{
}
public override async Task<IActionResult> OnPostAsync()
{
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.");
return Page();
}
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
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

@ -2,8 +2,12 @@
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>

View File

@ -230,6 +230,33 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
await UserStories.LoginExistingUserAsync(newClient, userName, password);
}
[Fact]
public async Task CanResendConfirmingEmail()
{
// Arrange
var emailSender = new ContosoEmailSender();
void ConfigureTestServices(IServiceCollection services) => services
.SetupTestEmailSender(emailSender)
.SetupEmailRequired();
var server = ServerFactory.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
var client = server.CreateClient();
var newClient = server.CreateClient();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password);
// Act & Assert
// Use a new client to simulate a new browser session.
await UserStories.ResendConfirmEmailAsync(server.CreateClient(), userName);
Assert.Equal(2, emailSender.SentEmails.Count);
var email = emailSender.SentEmails.Last();
await UserStories.ConfirmEmailAsync(email, newClient);
}
[Fact]
public async Task CanLogInAfterConfirmingEmail_WithGlobalAuthorizeFilter()
{

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{
private readonly IHtmlFormElement _loginForm;
private readonly IHtmlAnchorElement _forgotPasswordLink;
private readonly IHtmlAnchorElement _reconfirmLink;
private readonly IHtmlFormElement _externalLoginForm;
private readonly IHtmlElement _contosoButton;
private readonly IHtmlElement _loginButton;
@ -26,6 +27,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
_loginForm = HtmlAssert.HasForm("#account", login);
_loginButton = HtmlAssert.HasElement("#login-submit", login);
_forgotPasswordLink = HtmlAssert.HasLink("#forgot-password", login);
_reconfirmLink = HtmlAssert.HasLink("#resend-confirmation", login);
if (Context.ContosoLoginEnabled)
{
_externalLoginForm = HtmlAssert.HasForm("#external-account", login);
@ -52,6 +54,14 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
return new ForgotPassword(Client, forgotPassword, Context);
}
public async Task<ResendEmailConfirmation> ClickReconfirmEmailLinkAsync()
{
var response = await Client.GetAsync(_reconfirmLink.Href);
var forgotPassword = await ResponseAssert.IsHtmlDocumentAsync(response);
return new ResendEmailConfirmation(Client, forgotPassword, Context);
}
public async Task<Index> LoginValidUserAsync(string userName, string password)
{
var loggedIn = await SendLoginForm(userName, password);

View File

@ -0,0 +1,26 @@
// 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;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{
public class ResendEmailConfirmation : DefaultUIPage
{
private readonly IHtmlFormElement _resendForm;
public ResendEmailConfirmation(HttpClient client, IHtmlDocument document, DefaultUIContext context) : base(client, document, context)
{
_resendForm = HtmlAssert.HasForm(document);
}
public Task<HttpResponseMessage> ResendAsync(string email)
=> Client.SendAsync(_resendForm, new Dictionary<string, string>
{
["Input_Email"] = email
});
}
}

View File

@ -194,6 +194,16 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
.WithConfirmedEmail());
}
internal static async Task ResendConfirmEmailAsync(HttpClient client, string email)
{
var index = await Index.CreateAsync(client);
var login = await index.ClickLoginLinkAsync();
var reconfirm = await login.ClickReconfirmEmailLinkAsync();
var response = await reconfirm.ResendAsync(email);
ResponseAssert.IsOK(response);
Assert.Contains("Verification email sent.", await response.Content.ReadAsStringAsync());
}
internal static async Task<ForgotPasswordConfirmation> ForgotPasswordAsync(HttpClient client, string userName)
{
var index = await Index.CreateAsync(client);

View File

@ -12,6 +12,7 @@ namespace Templates.Test.Helpers
public const string LoginUrl = "/Identity/Account/Login";
public const string RegisterUrl = "/Identity/Account/Register";
public const string ForgotPassword = "/Identity/Account/ForgotPassword";
public const string ResendEmailConfirmation = "/Identity/Account/ResendEmailConfirmation";
public const string ExternalArticle = "https://go.microsoft.com/fwlink/?LinkID=532715";
}
}

View File

@ -182,6 +182,7 @@ namespace Templates.Test
PageUrls.PrivacyUrl,
PageUrls.ForgotPassword,
PageUrls.RegisterUrl,
PageUrls.ResendEmailConfirmation,
PageUrls.ExternalArticle,
PageUrls.PrivacyUrl }
},

View File

@ -172,6 +172,7 @@ namespace Templates.Test
PageUrls.PrivacyUrl,
PageUrls.ForgotPassword,
PageUrls.RegisterUrl,
PageUrls.ResendEmailConfirmation,
PageUrls.ExternalArticle,
PageUrls.PrivacyUrl }
},