Skip to content

Commit 4119513

Browse files
authored
Fix resend email confirmation (#14118)
1 parent b617083 commit 4119513

File tree

15 files changed

+384
-2
lines changed

15 files changed

+384
-2
lines changed

src/Identity/UI/ref/Microsoft.AspNetCore.Identity.UI.netcoreapp.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,22 @@ public InputModel() { }
207207
}
208208
}
209209
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
210+
public abstract partial class ResendEmailConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
211+
{
212+
protected ResendEmailConfirmationModel() { }
213+
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
214+
public Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal.ResendEmailConfirmationModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
215+
public virtual void OnGet() { }
216+
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
217+
public partial class InputModel
218+
{
219+
public InputModel() { }
220+
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
221+
[System.ComponentModel.DataAnnotations.RequiredAttribute]
222+
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
223+
}
224+
}
225+
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
210226
public partial class ResetPasswordConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
211227
{
212228
public ResetPasswordConfirmationModel() { }
@@ -657,6 +673,22 @@ public InputModel() { }
657673
}
658674
}
659675
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
676+
public abstract partial class ResendEmailConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
677+
{
678+
protected ResendEmailConfirmationModel() { }
679+
[Microsoft.AspNetCore.Mvc.BindPropertyAttribute]
680+
public Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal.ResendEmailConfirmationModel.InputModel Input { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
681+
public virtual void OnGet() { }
682+
public virtual System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.IActionResult> OnPostAsync() { throw null; }
683+
public partial class InputModel
684+
{
685+
public InputModel() { }
686+
[System.ComponentModel.DataAnnotations.EmailAddressAttribute]
687+
[System.ComponentModel.DataAnnotations.RequiredAttribute]
688+
public string Email { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
689+
}
690+
}
691+
[Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute]
660692
public partial class ResetPasswordConfirmationModel : Microsoft.AspNetCore.Mvc.RazorPages.PageModel
661693
{
662694
public ResetPasswordConfirmationModel() { }

src/Identity/UI/src/Areas/Identity/Pages/V3/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+
<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">Resend</button>
20+
</form>
21+
</div>
22+
</div>
23+
24+
@section Scripts {
25+
<partial name="_ValidationScriptsPartial" />
26+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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;
7+
using System.Text.Encodings.Web;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Authorization;
10+
using Microsoft.AspNetCore.Identity.UI.Services;
11+
using Microsoft.AspNetCore.Mvc;
12+
using Microsoft.AspNetCore.Mvc.RazorPages;
13+
using Microsoft.AspNetCore.WebUtilities;
14+
15+
namespace Microsoft.AspNetCore.Identity.UI.V3.Pages.Account.Internal
16+
{
17+
/// <summary>
18+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
19+
/// directly from your code. This API may change or be removed in future releases.
20+
/// </summary>
21+
[AllowAnonymous]
22+
[IdentityDefaultUI(typeof(ResendEmailConfirmationModel<>))]
23+
public abstract class ResendEmailConfirmationModel : PageModel
24+
{
25+
/// <summary>
26+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
27+
/// directly from your code. This API may change or be removed in future releases.
28+
/// </summary>
29+
[BindProperty]
30+
public InputModel Input { get; set; }
31+
32+
/// <summary>
33+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
34+
/// directly from your code. This API may change or be removed in future releases.
35+
/// </summary>
36+
public class InputModel
37+
{
38+
/// <summary>
39+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
40+
/// directly from your code. This API may change or be removed in future releases.
41+
/// </summary>
42+
[Required]
43+
[EmailAddress]
44+
public string Email { get; set; }
45+
}
46+
47+
/// <summary>
48+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
49+
/// directly from your code. This API may change or be removed in future releases.
50+
/// </summary>
51+
public virtual void OnGet() => throw new NotImplementedException();
52+
53+
/// <summary>
54+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
55+
/// directly from your code. This API may change or be removed in future releases.
56+
/// </summary>
57+
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
58+
}
59+
60+
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
61+
{
62+
private readonly UserManager<TUser> _userManager;
63+
private readonly IEmailSender _emailSender;
64+
65+
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
66+
{
67+
_userManager = userManager;
68+
_emailSender = emailSender;
69+
}
70+
71+
public override void OnGet()
72+
{
73+
}
74+
75+
public override async Task<IActionResult> OnPostAsync()
76+
{
77+
if (!ModelState.IsValid)
78+
{
79+
return Page();
80+
}
81+
82+
var user = await _userManager.FindByEmailAsync(Input.Email);
83+
if (user == null)
84+
{
85+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
86+
return Page();
87+
}
88+
89+
var userId = await _userManager.GetUserIdAsync(user);
90+
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
91+
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
92+
var callbackUrl = Url.Page(
93+
"/Account/ConfirmEmail",
94+
pageHandler: null,
95+
values: new { userId = userId, code = code },
96+
protocol: Request.Scheme);
97+
await _emailSender.SendEmailAsync(
98+
Input.Email,
99+
"Confirm your email",
100+
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
101+
102+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
103+
return Page();
104+
}
105+
}
106+
}

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">Resend</button>
20+
</form>
21+
</div>
22+
</div>
23+
24+
@section Scripts {
25+
<partial name="_ValidationScriptsPartial" />
26+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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;
7+
using System.Text.Encodings.Web;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Authorization;
10+
using Microsoft.AspNetCore.Identity.UI.Services;
11+
using Microsoft.AspNetCore.Mvc;
12+
using Microsoft.AspNetCore.Mvc.RazorPages;
13+
using Microsoft.AspNetCore.WebUtilities;
14+
15+
namespace Microsoft.AspNetCore.Identity.UI.V4.Pages.Account.Internal
16+
{
17+
/// <summary>
18+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
19+
/// directly from your code. This API may change or be removed in future releases.
20+
/// </summary>
21+
[AllowAnonymous]
22+
[IdentityDefaultUI(typeof(ResendEmailConfirmationModel<>))]
23+
public abstract class ResendEmailConfirmationModel : PageModel
24+
{
25+
/// <summary>
26+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
27+
/// directly from your code. This API may change or be removed in future releases.
28+
/// </summary>
29+
[BindProperty]
30+
public InputModel Input { get; set; }
31+
32+
/// <summary>
33+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
34+
/// directly from your code. This API may change or be removed in future releases.
35+
/// </summary>
36+
public class InputModel
37+
{
38+
/// <summary>
39+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
40+
/// directly from your code. This API may change or be removed in future releases.
41+
/// </summary>
42+
[Required]
43+
[EmailAddress]
44+
public string Email { get; set; }
45+
}
46+
47+
/// <summary>
48+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
49+
/// directly from your code. This API may change or be removed in future releases.
50+
/// </summary>
51+
public virtual void OnGet() => throw new NotImplementedException();
52+
53+
/// <summary>
54+
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
55+
/// directly from your code. This API may change or be removed in future releases.
56+
/// </summary>
57+
public virtual Task<IActionResult> OnPostAsync() => throw new NotImplementedException();
58+
}
59+
60+
internal class ResendEmailConfirmationModel<TUser> : ResendEmailConfirmationModel where TUser : class
61+
{
62+
private readonly UserManager<TUser> _userManager;
63+
private readonly IEmailSender _emailSender;
64+
65+
public ResendEmailConfirmationModel(UserManager<TUser> userManager, IEmailSender emailSender)
66+
{
67+
_userManager = userManager;
68+
_emailSender = emailSender;
69+
}
70+
71+
public override void OnGet()
72+
{
73+
}
74+
75+
public override async Task<IActionResult> OnPostAsync()
76+
{
77+
if (!ModelState.IsValid)
78+
{
79+
return Page();
80+
}
81+
82+
var user = await _userManager.FindByEmailAsync(Input.Email);
83+
if (user == null)
84+
{
85+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
86+
return Page();
87+
}
88+
89+
var userId = await _userManager.GetUserIdAsync(user);
90+
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
91+
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
92+
var callbackUrl = Url.Page(
93+
"/Account/ConfirmEmail",
94+
pageHandler: null,
95+
values: new { userId = userId, code = code },
96+
protocol: Request.Scheme);
97+
await _emailSender.SendEmailAsync(
98+
Input.Email,
99+
"Confirm your email",
100+
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
101+
102+
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
103+
return Page();
104+
}
105+
}
106+
}

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
{

0 commit comments

Comments
 (0)