Skip to content

Commit 8d38c26

Browse files
committed
Fix resend email confirmation
1 parent 7401e0b commit 8d38c26

File tree

13 files changed

+346
-37
lines changed

13 files changed

+346
-37
lines changed

src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
4343
</p>
4444
<p>
45-
<button id="resend-confirmation" type="submit" asp-page-handler="SendVerificationEmail" class="btn-link" style="padding:0px;margin:0px;border:0px">Resend email confirmation</button>
45+
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
4646
</p>
4747
</div>
4848
</form>

src/Identity/UI/src/Areas/Identity/Pages/V3/Account/Login.cshtml.cs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -169,34 +169,5 @@ public override async Task<IActionResult> OnPostAsync(string returnUrl = null)
169169
// If we got this far, something failed, redisplay form
170170
return Page();
171171
}
172-
173-
public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
174-
{
175-
if (!ModelState.IsValid)
176-
{
177-
return Page();
178-
}
179-
180-
var user = await _userManager.FindByEmailAsync(Input.Email);
181-
if (user == null)
182-
{
183-
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
184-
}
185-
186-
var userId = await _userManager.GetUserIdAsync(user);
187-
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
188-
var callbackUrl = Url.Page(
189-
"/Account/ConfirmEmail",
190-
pageHandler: null,
191-
values: new { userId = userId, code = code },
192-
protocol: Request.Scheme);
193-
await _emailSender.SendEmailAsync(
194-
Input.Email,
195-
"Confirm your email",
196-
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
197-
198-
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
199-
return Page();
200-
}
201172
}
202173
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@page
2+
@model ResendEmailConfirmationModel
3+
@{
4+
ViewData["Title"] = "Resend email confirmation";
5+
}
6+
7+
<h2>@ViewData["Title"]</h2>
8+
<h4>Enter your email.</h4>
9+
<hr />
10+
<div class="row">
11+
<div class="col-md-4">
12+
<form method="post">
13+
<div asp-validation-summary="All" class="text-danger"></div>
14+
<div class="form-group">
15+
<label asp-for="Input.Email"></label>
16+
<input asp-for="Input.Email" class="form-control" />
17+
<span asp-validation-for="Input.Email" class="text-danger"></span>
18+
</div>
19+
<button type="submit" class="btn btn-default">Reset</button>
20+
</form>
21+
</div>
22+
</div>
23+
24+
@section Scripts {
25+
<partial name="_ValidationScriptsPartial" />
26+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.ComponentModel.DataAnnotations;
6+
using System.Text.Encodings.Web;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Identity.UI.Services;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.AspNetCore.Mvc.RazorPages;
12+
13+
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
14+
{
15+
/// <summary>
16+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
17+
/// directly from your code. This API may change or be removed in future releases.
18+
/// </summary>
19+
[AllowAnonymous]
20+
[IdentityDefaultUI(typeof(ResendEmailConfirmationModel<>))]
21+
public abstract class ResendEmailConfirmationModel : PageModel
22+
{
23+
/// <summary>
24+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
25+
/// directly from your code. This API may change or be removed in future releases.
26+
/// </summary>
27+
[BindProperty]
28+
public InputModel Input { get; set; }
29+
30+
/// <summary>
31+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
32+
/// directly from your code. This API may change or be removed in future releases.
33+
/// </summary>
34+
public class InputModel
35+
{
36+
/// <summary>
37+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
38+
/// directly from your code. This API may change or be removed in future releases.
39+
/// </summary>
40+
[Required]
41+
[EmailAddress]
42+
public string Email { get; set; }
43+
}
44+
45+
/// <summary>
46+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
47+
/// directly from your code. This API may change or be removed in future releases.
48+
/// </summary>
49+
public virtual void OnGet() => throw new NotImplementedException();
50+
51+
/// <summary>
52+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
53+
/// directly from your code. This API may change or be removed in future releases.
54+
/// </summary>
55+
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
56+
}
57+
58+
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
59+
{
60+
private readonly UserManager<TUser> _userManager;
61+
private readonly IEmailSender _emailSender;
62+
63+
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
64+
{
65+
_userManager = userManager;
66+
_emailSender = emailSender;
67+
}
68+
69+
public override void OnGet()
70+
{
71+
}
72+
73+
public override async Task<IActionResult> OnPostAsync()
74+
{
75+
if (!ModelState.IsValid)
76+
{
77+
return Page();
78+
}
79+
80+
var user = await _userManager.FindByEmailAsync(Input.Email);
81+
if (user == null)
82+
{
83+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
84+
return Page();
85+
}
86+
87+
var userId = await _userManager.GetUserIdAsync(user);
88+
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
89+
var callbackUrl = Url.Page(
90+
"/Account/ConfirmEmail",
91+
pageHandler: null,
92+
values: new { userId = userId, code = code },
93+
protocol: Request.Scheme);
94+
await _emailSender.SendEmailAsync(
95+
Input.Email,
96+
"Confirm your email",
97+
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
98+
99+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
100+
return Page();
101+
}
102+
}
103+
}

src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
<p>
4242
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
4343
</p>
44+
<p>
45+
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
46+
</p>
4447
</div>
4548
</form>
4649
</section>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@page
2+
@model ResendEmailConfirmationModel
3+
@{
4+
ViewData["Title"] = "Resend email confirmation";
5+
}
6+
7+
<h1>@ViewData["Title"]</h1>
8+
<h4>Enter your email.</h4>
9+
<hr />
10+
<div class="row">
11+
<div class="col-md-4">
12+
<form method="post">
13+
<div asp-validation-summary="All" class="text-danger"></div>
14+
<div class="form-group">
15+
<label asp-for="Input.Email"></label>
16+
<input asp-for="Input.Email" class="form-control" />
17+
<span asp-validation-for="Input.Email" class="text-danger"></span>
18+
</div>
19+
<button type="submit" class="btn btn-primary">Reset</button>
20+
</form>
21+
</div>
22+
</div>
23+
24+
@section Scripts {
25+
<partial name="_ValidationScriptsPartial" />
26+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.ComponentModel.DataAnnotations;
6+
using System.Text.Encodings.Web;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Identity.UI.Services;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.AspNetCore.Mvc.RazorPages;
12+
13+
namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
14+
{
15+
/// <summary>
16+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
17+
/// directly from your code. This API may change or be removed in future releases.
18+
/// </summary>
19+
[AllowAnonymous]
20+
[IdentityDefaultUI(typeof(ResendEmailConfirmationModel<>))]
21+
public abstract class ResendEmailConfirmationModel : PageModel
22+
{
23+
/// <summary>
24+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
25+
/// directly from your code. This API may change or be removed in future releases.
26+
/// </summary>
27+
[BindProperty]
28+
public InputModel Input { get; set; }
29+
30+
/// <summary>
31+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
32+
/// directly from your code. This API may change or be removed in future releases.
33+
/// </summary>
34+
public class InputModel
35+
{
36+
/// <summary>
37+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
38+
/// directly from your code. This API may change or be removed in future releases.
39+
/// </summary>
40+
[Required]
41+
[EmailAddress]
42+
public string Email { get; set; }
43+
}
44+
45+
/// <summary>
46+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
47+
/// directly from your code. This API may change or be removed in future releases.
48+
/// </summary>
49+
public virtual void OnGet() => throw new NotImplementedException();
50+
51+
/// <summary>
52+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
53+
/// directly from your code. This API may change or be removed in future releases.
54+
/// </summary>
55+
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
56+
}
57+
58+
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
59+
{
60+
private readonly UserManager<TUser> _userManager;
61+
private readonly IEmailSender _emailSender;
62+
63+
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
64+
{
65+
_userManager = userManager;
66+
_emailSender = emailSender;
67+
}
68+
69+
public override void OnGet()
70+
{
71+
}
72+
73+
public override async Task<IActionResult> OnPostAsync()
74+
{
75+
if (!ModelState.IsValid)
76+
{
77+
return Page();
78+
}
79+
80+
var user = await _userManager.FindByEmailAsync(Input.Email);
81+
if (user == null)
82+
{
83+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
84+
return Page();
85+
}
86+
87+
var userId = await _userManager.GetUserIdAsync(user);
88+
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
89+
var callbackUrl = Url.Page(
90+
"/Account/ConfirmEmail",
91+
pageHandler: null,
92+
values: new { userId = userId, code = code },
93+
protocol: Request.Scheme);
94+
await _emailSender.SendEmailAsync(
95+
Input.Email,
96+
"Confirm your email",
97+
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
98+
99+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
100+
return Page();
101+
}
102+
}
103+
}

src/Identity/samples/IdentitySample.DefaultUI/web.config

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
<configuration>
33
<system.webServer>
44
<handlers>
5-
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
5+
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
66
</handlers>
7-
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
7+
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false">
8+
<environmentVariables>
9+
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
10+
</environmentVariables>
11+
</aspNetCore>
812
</system.webServer>
913
</configuration>

src/Identity/test/Identity.FunctionalTests/LoginTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,33 @@ void ConfigureTestServices(IServiceCollection services) => services
230230
await UserStories.LoginExistingUserAsync(newClient, userName, password);
231231
}
232232

233+
[Fact]
234+
public async Task CanResendConfirmingEmail()
235+
{
236+
// Arrange
237+
var emailSender = new ContosoEmailSender();
238+
void ConfigureTestServices(IServiceCollection services) => services
239+
.SetupTestEmailSender(emailSender)
240+
.SetupEmailRequired();
241+
242+
var server = ServerFactory.WithWebHostBuilder(whb => whb.ConfigureServices(ConfigureTestServices));
243+
244+
var client = server.CreateClient();
245+
var newClient = server.CreateClient();
246+
247+
var userName = $"{Guid.NewGuid()}@example.com";
248+
var password = $"!Test.Password1$";
249+
250+
var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password);
251+
252+
// Act & Assert
253+
// Use a new client to simulate a new browser session.
254+
await UserStories.ResendConfirmEmailAsync(server.CreateClient(), userName);
255+
Assert.Equal(2, emailSender.SentEmails.Count);
256+
var email = emailSender.SentEmails.Last();
257+
await UserStories.ConfirmEmailAsync(email, newClient);
258+
}
259+
233260
[Fact]
234261
public async Task CanLogInAfterConfirmingEmail_WithGlobalAuthorizeFilter()
235262
{

src/Identity/test/Identity.FunctionalTests/Pages/Account/Login.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class Login : DefaultUIPage
1313
{
1414
private readonly IHtmlFormElement _loginForm;
1515
private readonly IHtmlAnchorElement _forgotPasswordLink;
16+
private readonly IHtmlAnchorElement _reconfirmLink;
1617
private readonly IHtmlFormElement _externalLoginForm;
1718
private readonly IHtmlElement _contosoButton;
1819
private readonly IHtmlElement _loginButton;
@@ -26,6 +27,7 @@ public Login(
2627
_loginForm = HtmlAssert.HasForm("#account", login);
2728
_loginButton = HtmlAssert.HasElement("#login-submit", login);
2829
_forgotPasswordLink = HtmlAssert.HasLink("#forgot-password", login);
30+
_reconfirmLink = HtmlAssert.HasLink("#resend-confirmation", login);
2931
if (Context.ContosoLoginEnabled)
3032
{
3133
_externalLoginForm = HtmlAssert.HasForm("#external-account", login);
@@ -52,6 +54,14 @@ public async Task<ForgotPassword> ClickForgotPasswordLinkAsync()
5254
return new ForgotPassword(Client, forgotPassword, Context);
5355
}
5456

57+
public async Task<ResendEmailConfirmation> ClickReconfirmEmailLinkAsync()
58+
{
59+
var response = await Client.GetAsync(_reconfirmLink.Href);
60+
var forgotPassword = await ResponseAssert.IsHtmlDocumentAsync(response);
61+
62+
return new ResendEmailConfirmation(Client, forgotPassword, Context);
63+
}
64+
5565
public async Task<Index> LoginValidUserAsync(string userName, string password)
5666
{
5767
var loggedIn = await SendLoginForm(userName, password);

0 commit comments

Comments
 (0)